From 93ee6996011f78a7544f80684f4b29afb9d566f6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Dec 2022 15:58:06 -0500 Subject: [PATCH 001/196] changes for charge spilling from saturated core --- src/stcal/jump/jump.py | 58 ++++++++++++++++++++++++++++++++++-------- tests/test_jump.py | 50 ++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 1ab252b2..79e5a7c4 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -1,6 +1,7 @@ import time import logging import warnings +from astropy.io import fits import numpy as np from . import twopoint_difference as twopt @@ -320,7 +321,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, expand_factor=1.9, use_ellipses=False, - sat_required_snowball=True): + sat_required_snowball=True, min_sat_radius_extend=2, sat_expand=1): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called snowballs for the NIR and are almost always circular with a saturated core. For MIRI they are better @@ -352,20 +353,31 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, gdq[integration, group, :, :], num_events = \ extend_ellipses(gdq[integration, group, :, :], jump_ellipses, sat_flag, jump_flag, - expansion=expand_factor) + expansion=sat_expand) else: - # this line needs to be changed later to just look at saturation flags. new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] - + # find the circle parameters for newly saturated pixels sat_circles = find_circles(new_flagged_pixels, sat_flag, min_sat_area) - sat_pixels = np.bitwise_and(new_flagged_pixels, sat_flag) + # expand the larger saturated cores to deal with the charge migration from the + # saturated cores. +# gdq[integration, , :, :] = extend_saturation(gdq[integration, :, :, :], + gdq[integration, :, :, :] = extend_saturation(gdq[integration, :, :, :], + group, sat_circles, sat_flag, jump_flag, + min_sat_radius_extend, expansion=sat_expand) + fits.writeto("after_extend_large_events.fits", gdq, overwrite=True) + # recalculate the newly flagged pixels after the expansion of saturation + new_flagged_pixels2 = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] + sat_circles2 = find_circles(new_flagged_pixels2, sat_flag, min_sat_area) + # find all the newlay saturated pixel + sat_pixels = np.bitwise_and(new_flagged_pixels2, sat_flag) saty, satx = np.where(sat_pixels == sat_flag) only_jump = gdq[integration, group, :, :].copy() + # reset the saturated pixel to be jump to allow the jump circles to have the + # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag jump_circles = find_circles(only_jump, jump_flag, min_jump_area) - if sat_required_snowball: - snowballs = make_snowballs(jump_circles, sat_circles) + snowballs = make_snowballs(jump_circles, sat_circles2) else: snowballs = jump_circles n_showers_grp.append(len(snowballs)) @@ -392,7 +404,7 @@ def extend_snowballs(plane, snowballs, sat_flag, jump_flag, expansion=1.5): # the jump flag set. image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) num_circles = len(snowballs) - sat_pix = np.bitwise_and(plane, 2) + sat_pix = np.bitwise_and(plane, sat_flag) for snowball in snowballs: jump_radius = snowball[1] jump_center = snowball[0] @@ -401,19 +413,43 @@ def extend_snowballs(plane, snowballs, sat_flag, jump_flag, expansion=1.5): extend_radius = round(jump_radius * expansion) image = cv.circle(image, (round(ceny), round(cenx)), extend_radius, (0, 0, 4), -1) jump_circle = image[:, :, 2] - saty, satx = np.where(sat_pix == 2) + saty, satx = np.where(sat_pix == sat_flag) jump_circle[saty, satx] = 0 plane = np.bitwise_or(plane, jump_circle) return plane, num_circles +def extend_saturation(cube, grp, sat_circles, sat_flag, jump_flag, + min_sat_radius_extend, expansion=1): + image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) + jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) + print("Grp in ES", grp) + count = 0 + for sat_circle in sat_circles: + radius = sat_circle[1] + count = count +1 + print("Grp", grp, " radius ", radius, "count", count) + if radius > min_sat_radius_extend: + new_radius = round(radius + expansion) + sat_center = sat_circle[0] + cenx = sat_center[1] + ceny = sat_center[0] + image = cv.circle(image, (round(ceny), round(cenx)), new_radius, (0, 0, 4), -1) + sat_circle = image[:, :, 2] + saty, satx = np.where(sat_circle == 4) + cube[grp:, saty, satx] = sat_flag + return cube + + + + def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1): # For a given DQ plane it will use the list of ellipses to create expanded ellipses of pixels with # the jump flag set. image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) num_ellipses = len(ellipses) - sat_pix = np.bitwise_and(plane, 2) + sat_pix = np.bitwise_and(plane, sat_flag) for ellipse in ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] @@ -431,7 +467,7 @@ def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1): image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), round(axis2 / 2)), alpha, 0, 360, (0, 0, 4), -1) jump_ellipse = image[:, :, 2] - saty, satx = np.where(sat_pix == 2) + saty, satx = np.where(sat_pix == sat_flag) jump_ellipse[saty, satx] = 0 plane = np.bitwise_or(plane, jump_ellipse) return plane, num_ellipses diff --git a/tests/test_jump.py b/tests/test_jump.py index af69e080..5f4e043c 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -2,7 +2,7 @@ import pytest from astropy.io import fits -from stcal.jump.jump import flag_large_events, find_circles, find_ellipses +from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1} @@ -59,6 +59,52 @@ def test_find_simple_ellipse(): assert ellipse[0][0] == pytest.approx((2.5, 2.0)) # center +def test_extend_saturation_simple(): + cube = np.zeros(shape=(5, 7, 7), dtype=np.uint8) + grp = 1 + min_sat_radius_extend = 1 + cube[1, 3, 3] = DQFLAGS['SATURATED'] + cube[1, 2, 3] = DQFLAGS['SATURATED'] + cube[1, 3, 4] = DQFLAGS['SATURATED'] + cube[1, 4, 3] = DQFLAGS['SATURATED'] + cube[1, 3, 2] = DQFLAGS['SATURATED'] + cube[1, 2, 2] = DQFLAGS['JUMP_DET'] + fits.writeto("start_sat_extend.fits", cube, overwrite=True) + sat_circles = find_circles(cube[grp, :, :], DQFLAGS['SATURATED'], 1) + new_cube = extend_saturation(cube, grp, sat_circles, DQFLAGS['SATURATED'], DQFLAGS['JUMP_DET'], + min_sat_radius_extend, expansion=1) + assert cube[grp, 2, 2] == DQFLAGS['SATURATED'] + assert cube[grp, 3, 5] == DQFLAGS['SATURATED'] + assert cube[grp, 3, 6] == 0 + fits.writeto("out_sat_extend.fits", cube, overwrite=True) + + + +def test_flag_large_events(): + cube = np.zeros(shape=(1, 5, 7, 7), dtype=np.uint8) + grp = 1 + min_sat_radius_extend = 1 + cube[0, 1, 3, 3] = DQFLAGS['SATURATED'] + cube[0, 1, 2, 3] = DQFLAGS['SATURATED'] + cube[0, 1, 3, 4] = DQFLAGS['SATURATED'] + cube[0, 1, 4, 3] = DQFLAGS['SATURATED'] + cube[0, 1, 3, 2] = DQFLAGS['SATURATED'] + cube[0, 2, 3, 4] = DQFLAGS['SATURATED'] + cube[0, 2, 2, 4] = DQFLAGS['SATURATED'] + cube[0, 2, 3, 4] = DQFLAGS['SATURATED'] + cube[0, 2, 4, 4] = DQFLAGS['SATURATED'] + cube[0, 2, 3, 4] = DQFLAGS['SATURATED'] + fits.writeto("start_sat_extend2.fits", cube, overwrite=True) + flag_large_events(cube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=6, + expand_factor=1.9, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=1, sat_expand=1) + fits.writeto("out_flag_large_events.fits", cube, overwrite=True) + assert cube[0, 1, 2, 2] == DQFLAGS['SATURATED'] + assert cube[0, 1, 3, 5] == DQFLAGS['SATURATED'] + assert cube[0, 1, 3, 6] == 0 + fits.writeto("out_flag_large_events.fits", cube, overwrite=True) + @pytest.mark.skip(reason="only for local testing") def test_single_group(): inplane = fits.getdata("jumppix.fits") @@ -66,5 +112,5 @@ def test_single_group(): indq[0, 0, :, :] = inplane flag_large_events(indq, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, min_jump_area=15, max_offset=1, expand_factor=1.1, use_ellipses=True, - sat_required_snowball=False) + sat_required_snowball=False, min_sat_radius_extend=1) fits.writeto("jumppix_expand.fits", indq, overwrite=True) From 533ec24fefc8999b7783f6fb6c87808ca9b53bf0 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Dec 2022 17:23:10 -0500 Subject: [PATCH 002/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 79e5a7c4..a80bd018 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -341,7 +341,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, :return: none """ - log.info('Flagging large events (snowballs, showers).') + log.info('TEST Flagging large events (snowballs, showers).') n_showers_grp = [] n_showers_grp_ellipse = [] From 7d4f07e29248de9a7a936a1439c83a1eb14c87ba Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 22 Dec 2022 16:42:37 -0500 Subject: [PATCH 003/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index a80bd018..67f12563 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -321,7 +321,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, expand_factor=1.9, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2, sat_expand=1): + sat_required_snowball=True, min_sat_radius_extend=2, sat_expand=2): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called snowballs for the NIR and are almost always circular with a saturated core. For MIRI they are better From dc8f5212f43d8868bd7bcb566ec65bb1ec55b81b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 3 Jan 2023 14:41:11 -0500 Subject: [PATCH 004/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 67f12563..3385155b 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -320,7 +320,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, - expand_factor=1.9, use_ellipses=False, + expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2, sat_expand=2): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called From 7c064ceb8b8cdebcc9205fe941922844824edd1e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 3 Jan 2023 15:53:42 -0500 Subject: [PATCH 005/196] Update saturation.py --- src/stcal/saturation/saturation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/saturation/saturation.py b/src/stcal/saturation/saturation.py index 71a27b5f..30975e13 100644 --- a/src/stcal/saturation/saturation.py +++ b/src/stcal/saturation/saturation.py @@ -61,7 +61,7 @@ def flag_saturated_pixels( """ nints, ngroups, nrows, ncols = data.shape - + print("local version of saturation") saturated = dqflags['SATURATED'] ad_floor = dqflags['AD_FLOOR'] no_sat_check = dqflags['NO_SAT_CHECK'] From f7880c6881c01397d136327e45f75c51401cfe13 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 4 Jan 2023 10:47:01 -0500 Subject: [PATCH 006/196] Update saturation.py --- src/stcal/saturation/saturation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/saturation/saturation.py b/src/stcal/saturation/saturation.py index 30975e13..45f92f58 100644 --- a/src/stcal/saturation/saturation.py +++ b/src/stcal/saturation/saturation.py @@ -62,6 +62,7 @@ def flag_saturated_pixels( nints, ngroups, nrows, ncols = data.shape print("local version of saturation") + stop saturated = dqflags['SATURATED'] ad_floor = dqflags['AD_FLOOR'] no_sat_check = dqflags['NO_SAT_CHECK'] From 7f04b48d0c8315b6592b614fe4e59a426430bb17 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 11:42:43 -0500 Subject: [PATCH 007/196] use ellipses for all jumps --- src/stcal/jump/jump.py | 63 ++++++++++++++++++++++++++++++++++-------- tests/test_jump.py | 28 ++++++++++++++++--- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 3385155b..6b4651e9 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -348,7 +348,8 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): if use_ellipses: - jump_ellipses = find_ellipses(gdq[integration, group, :, :], jump_flag, min_jump_area) + new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] + jump_ellipses = find_ellipses(new_flagged_pixels, jump_flag, min_jump_area) n_showers_grp_ellipse.append(len(jump_ellipses)) gdq[integration, group, :, :], num_events = \ extend_ellipses(gdq[integration, group, :, :], @@ -368,18 +369,18 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # recalculate the newly flagged pixels after the expansion of saturation new_flagged_pixels2 = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] sat_circles2 = find_circles(new_flagged_pixels2, sat_flag, min_sat_area) - # find all the newlay saturated pixel + # find all the newly saturated pixel sat_pixels = np.bitwise_and(new_flagged_pixels2, sat_flag) saty, satx = np.where(sat_pixels == sat_flag) only_jump = gdq[integration, group, :, :].copy() # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag - jump_circles = find_circles(only_jump, jump_flag, min_jump_area) + jump_ellipses = find_circles(only_jump, jump_flag, min_jump_area) if sat_required_snowball: - snowballs = make_snowballs(jump_circles, sat_circles2) + snowballs = make_snowballs(jump_ellipses, sat_circles2) else: - snowballs = jump_circles + snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) gdq[integration, group, :, :], num_events = extend_snowballs(gdq[integration, group, :, :], snowballs, sat_flag, @@ -495,15 +496,53 @@ def find_ellipses(dqplane, bitmask, min_area): return ellipses -def make_snowballs(jump_circles, sat_circles): - # Ths routine will create a list of snowballs (circles) that have the center of the saturation circle - # within the unclosing jump circle. +def make_snowballs(jump_ellipses, sat_circles): + # Ths routine will create a list of snowballs (ellipses) that have the center of the saturation circle + # within the enclosing jump rectangle. snowballs = [] - for jump in jump_circles: - radius = jump[1] + for jump in jump_ellipses: for sat in sat_circles: - distance = np.sqrt((jump[0][0] - sat[0][0]) ** 2 + (jump[0][1] - sat[0][1]) ** 2) - if distance < radius: # center of saturation is within the enclosing jump circle + # center of saturation is within the enclosing jump rectangle + if point_inside_rectangle(sat[0], jump): if jump not in snowballs: snowballs.append(jump) return snowballs + + +def point_inside_ellipse(pointy, pointx, ellipse): + box = cv.boxPoints(ellipse) + ceny = ellipse[0][0] + cenx = ellipse[0][1] + axis1 = ellipse[1][0] + axis2 = ellipse[1][1] + theta = np.deg2rad(ellipse[2]) + radius = ((np.cos(theta) * (pointx - cenx) + np.sin(theta) * (pointy - ceny))**2)/axis2**2 + \ + ((np.sin(theta) * (pointx - cenx) + np.cos(theta) * (pointy - ceny))**2)/axis1**2 + if radius < 1: + return True + else: + return False + + +def point_inside_rectangle(point, ellipse): + box = cv.boxPoints(ellipse) + area1 = triangle_area(point, box[0], box[1]) + area2 = triangle_area(point, box[1], box[2]) + area3 = triangle_area(point, box[2], box[3]) + area4 = triangle_area(point, box[3], box[0]) + rectangle_area = ellipse[1][0] * ellipse[1][1] + triangle_area_sum = area1 + area2 + area3 + area4 + if triangle_area_sum > rectangle_area: + return False + else: + return True + +#Area = abs( (Bx * Ay - Ax * By) + +# (Cx * By - Bx * Cy) + +# (Ax * Cy - Cx * Ay) ) / 2 +def triangle_area(point, vert1, vert2): + area = np.abs((vert1[1] * point[0] - point[1] * vert1[0]) + + (vert2[1] * vert1[0] - vert1[1] * vert2[0]) + + (point[1] * vert2[0] - vert2[1] * point[0])) / 2 + return area + diff --git a/tests/test_jump.py b/tests/test_jump.py index 5f4e043c..ecaa9e6e 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -2,7 +2,8 @@ import pytest from astropy.io import fits -from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation +from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation, \ + point_inside_ellipse, point_inside_rectangle DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1} @@ -111,6 +112,25 @@ def test_single_group(): indq = np.zeros(shape=(1, 1, inplane.shape[0], inplane.shape[1]), dtype=np.uint8) indq[0, 0, :, :] = inplane flag_large_events(indq, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=15, max_offset=1, expand_factor=1.1, use_ellipses=True, - sat_required_snowball=False, min_sat_radius_extend=1) - fits.writeto("jumppix_expand.fits", indq, overwrite=True) + min_jump_area=15, expand_factor=1.9, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=1) + fits.writeto("jumppix_expand.new.fits", indq, overwrite=True) + +def test_inside_ellipse3(): + ellipse = ((0, 0), (1, 2), -45) + point = (1, 2) + result = point_inside_rectangle(point, ellipse) + assert not result + + +def test_inside_ellipse5(): + ellipse = ((0, 0), (1, 2), -10) + point = (0.5, 1) + result = point_inside_rectangle(point, ellipse) + assert not result + +def test_inside_ellipse4(): + ellipse = ((0, 0), (1, 2), 0) + point = (0.5, 1) + result = point_inside_rectangle(point, ellipse) + assert result From 00c30a7aecbfbf1fb81430c085f2b167b50cba7b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 11:52:21 -0500 Subject: [PATCH 008/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 6b4651e9..ab26f3b4 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -376,7 +376,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag - jump_ellipses = find_circles(only_jump, jump_flag, min_jump_area) + jump_ellipses = find_ellipses(only_jump, jump_flag, min_jump_area) if sat_required_snowball: snowballs = make_snowballs(jump_ellipses, sat_circles2) else: From 14e3cf69554e05c0e5156c36d505f0c6732746d6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 11:58:36 -0500 Subject: [PATCH 009/196] Update jump.py --- src/stcal/jump/jump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index ab26f3b4..85dac830 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -354,6 +354,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, gdq[integration, group, :, :], num_events = \ extend_ellipses(gdq[integration, group, :, :], jump_ellipses, sat_flag, jump_flag, +############# is sat_expand the right parameter?????????????????? ########################### expansion=sat_expand) else: new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] @@ -382,7 +383,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) - gdq[integration, group, :, :], num_events = extend_snowballs(gdq[integration, group, :, :], + gdq[integration, group, :, :], num_events = extend_ellipses(gdq[integration, group, :, :], snowballs, sat_flag, jump_flag, expansion=expand_factor) From 44e1e07c4df16b7d38bde3f575f8ff5b59d24cb7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 12:52:47 -0500 Subject: [PATCH 010/196] Update jump.py --- src/stcal/jump/jump.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 85dac830..871ce7d4 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -345,6 +345,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] + only_jump_cube = np.zeros_like(gdq) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): if use_ellipses: @@ -377,6 +378,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag + only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump, jump_flag, min_jump_area) if sat_required_snowball: snowballs = make_snowballs(jump_ellipses, sat_circles2) @@ -387,6 +389,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, snowballs, sat_flag, jump_flag, expansion=expand_factor) + fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) if use_ellipses: if np.all(np.array(n_showers_grp_ellipse) == 0): log.info(f'No showers found in integration {integration}.') From e3a1c748bb9b35a2a30b901e9c46721c242f68d9 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 13:12:03 -0500 Subject: [PATCH 011/196] Update jump.py --- src/stcal/jump/jump.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 871ce7d4..73031ada 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -321,7 +321,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2, sat_expand=2): + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called snowballs for the NIR and are almost always circular with a saturated core. For MIRI they are better @@ -345,7 +345,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] - only_jump_cube = np.zeros_like(gdq) +# only_jump_cube = np.zeros_like(gdq) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): if use_ellipses: @@ -378,7 +378,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag - only_jump_cube[integration, group, :, :] = only_jump + # only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump, jump_flag, min_jump_area) if sat_required_snowball: snowballs = make_snowballs(jump_ellipses, sat_circles2) @@ -389,7 +389,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, snowballs, sat_flag, jump_flag, expansion=expand_factor) - fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) +# fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) if use_ellipses: if np.all(np.array(n_showers_grp_ellipse) == 0): log.info(f'No showers found in integration {integration}.') From 36e478a997a1baeba1d4c50f52bdb06822ebcb60 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 14:42:02 -0500 Subject: [PATCH 012/196] Update jump.py --- src/stcal/jump/jump.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 73031ada..3ec91d78 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -433,7 +433,7 @@ def extend_saturation(cube, grp, sat_circles, sat_flag, jump_flag, count = 0 for sat_circle in sat_circles: radius = sat_circle[1] - count = count +1 + count = count + 1 print("Grp", grp, " radius ", radius, "count", count) if radius > min_sat_radius_extend: new_radius = round(radius + expansion) @@ -505,11 +505,15 @@ def make_snowballs(jump_ellipses, sat_circles): # within the enclosing jump rectangle. snowballs = [] for jump in jump_ellipses: + sat_found = False for sat in sat_circles: # center of saturation is within the enclosing jump rectangle if point_inside_rectangle(sat[0], jump): if jump not in snowballs: snowballs.append(jump) + sat_found = True + if not sat_found: + print("no saturation within jump rectangle ", jump) return snowballs From 005a7e21988f7cff4e877775ca8fad0a4d387b9c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 15:28:03 -0500 Subject: [PATCH 013/196] Update jump.py --- src/stcal/jump/jump.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 3ec91d78..56864740 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -381,7 +381,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump, jump_flag, min_jump_area) if sat_required_snowball: - snowballs = make_snowballs(jump_ellipses, sat_circles2) + snowballs = make_snowballs(jump_ellipses, sat_circles2, group) else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) @@ -500,7 +500,7 @@ def find_ellipses(dqplane, bitmask, min_area): return ellipses -def make_snowballs(jump_ellipses, sat_circles): +def make_snowballs(jump_ellipses, sat_circles, grp): # Ths routine will create a list of snowballs (ellipses) that have the center of the saturation circle # within the enclosing jump rectangle. snowballs = [] @@ -512,8 +512,8 @@ def make_snowballs(jump_ellipses, sat_circles): if jump not in snowballs: snowballs.append(jump) sat_found = True - if not sat_found: - print("no saturation within jump rectangle ", jump) + if not sat_found and grp > 22: + print("no saturation within jump rectangle ", grp, jump) return snowballs From 7208ef53c4285088cc06a9f6ae4e72e0ad0b8516 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 15:52:53 -0500 Subject: [PATCH 014/196] Update jump.py --- src/stcal/jump/jump.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 56864740..6756c0f8 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -434,7 +434,7 @@ def extend_saturation(cube, grp, sat_circles, sat_flag, jump_flag, for sat_circle in sat_circles: radius = sat_circle[1] count = count + 1 - print("Grp", grp, " radius ", radius, "count", count) + print("Grp", grp, " radius ", radius, "count", count, "center", sat_circle[0]) if radius > min_sat_radius_extend: new_radius = round(radius + expansion) sat_center = sat_circle[0] @@ -512,7 +512,7 @@ def make_snowballs(jump_ellipses, sat_circles, grp): if jump not in snowballs: snowballs.append(jump) sat_found = True - if not sat_found and grp > 22: + if not sat_found and grp > 22 and jump[]: print("no saturation within jump rectangle ", grp, jump) return snowballs From 98219524e6574bd2f95769b5053056e6021b0d58 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 5 Jan 2023 15:54:11 -0500 Subject: [PATCH 015/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 6756c0f8..16789091 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -512,7 +512,7 @@ def make_snowballs(jump_ellipses, sat_circles, grp): if jump not in snowballs: snowballs.append(jump) sat_found = True - if not sat_found and grp > 22 and jump[]: + if not sat_found and grp > 22: print("no saturation within jump rectangle ", grp, jump) return snowballs From e3a5f7f3f0800fe661082962238b71e4dce65f66 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 10:41:39 -0500 Subject: [PATCH 016/196] Update jump.py --- src/stcal/jump/jump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 16789091..95436aa8 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -345,7 +345,8 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] -# only_jump_cube = np.zeros_like(gdq) + input_jump_cube = np.zeros_like(gdq) + fits.writeto("input_jump_cube.fits", input_jump_cube, overwrite=True) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): if use_ellipses: From 69d5a9a0e876388ebfa2386d45f5655fe092b9d7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 10:50:43 -0500 Subject: [PATCH 017/196] Update jump.py --- src/stcal/jump/jump.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 95436aa8..e53313c6 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -345,8 +345,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] - input_jump_cube = np.zeros_like(gdq) - fits.writeto("input_jump_cube.fits", input_jump_cube, overwrite=True) + fits.writeto("input_jump_cube.fits", gdq, overwrite=True) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): if use_ellipses: From b7efb11e0cf21fbbeac97dd77723fd40f4f2b0cc Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 14:31:42 -0500 Subject: [PATCH 018/196] fix sat expand --- src/.DS_Store | Bin 0 -> 6148 bytes src/stcal/.DS_Store | Bin 0 -> 6148 bytes src/stcal/jump/.DS_Store | Bin 0 -> 6148 bytes src/stcal/jump/jump.py | 71 ++++++++++++++++++++++++++------------- tests/.DS_Store | Bin 0 -> 6148 bytes tests/current_gdqfits | Bin 0 -> 33557760 bytes tests/test_jump.py | 13 ++++++- 7 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 src/.DS_Store create mode 100644 src/stcal/.DS_Store create mode 100644 src/stcal/jump/.DS_Store create mode 100644 tests/.DS_Store create mode 100644 tests/current_gdqfits diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..92b154c69b1134b55865669d1f434c936de40b6f GIT binary patch literal 6148 zcmeH~J&pn~427Thk&w2bq)fvBxIu(wPp}tYfpR1Y*gi+!XU7f0)@ZbPmYf$m@%((n zWDLNLKi31W1+byJV(-JmjPV+GJaEA`#_Rnuo^Q9&(^%wQ59qwc^Lj2zL_h>YKmhjAXg1hsgAT2otBW@wh( zgJr2j8{+vWr;Pd@<+{3zbh!?<32LanK-D>F3x2m}TV IBJfrMzvI~xi2wiq literal 0 HcmV?d00001 diff --git a/src/stcal/.DS_Store b/src/stcal/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c624cc58dfdace2fbdb8472da0dfb8d9d0d7aeab GIT binary patch literal 6148 zcmeH~JqiLr422WjLa^D=avBfd4F=H@cmYu>Q53{}j_%73f~&QNyg>3zG82}4#m+`V zba_84M0ydKz>PAuFfc{l$x-fdmh0iRKkY7;Q=Stb*@FJ*K=2U&>`->Y+Gh!{SOKib7DNT6(F#VZ`WRw$ zZwE`BtH~COcF`O@G@q=dnMM{&Ba=*Y2R#A8X54gYui6T;v4vKWw@omQT28b?DQlPP9&#l+9 ztDE9{24L&A-7T;Ju%J8Q-NV#;-+f|NE#qi$Mvn*dc*YaDN%iG~bC;~{@PZ?`U;J)2 z^!vxdVb7y+jaMEzFb=2m<@1!40#ZNAml_14MDd9N+>E&XJy ojdF%)#l&dETzD(K`Bqo_nftZjm>6{CgHF_sfa@ZY0)L^v2i7|pWdHyG literal 0 HcmV?d00001 diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index e53313c6..c4b6cef8 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -345,9 +345,10 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] - fits.writeto("input_jump_cube.fits", gdq, overwrite=True) +# fits.writeto("input_jump_cube.fits", gdq, overwrite=True) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): + print("Group ",group) if use_ellipses: new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] jump_ellipses = find_ellipses(new_flagged_pixels, jump_flag, min_jump_area) @@ -358,30 +359,46 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, ############# is sat_expand the right parameter?????????????????? ########################### expansion=sat_expand) else: - new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] + current_gdq = 1.0 * gdq[integration, group, :, :] + prev_gdq = 1.0 * gdq[integration, group - 1, :, :] + diff_gdq = 1.0 * current_gdq - prev_gdq + diff_gdq[diff_gdq != sat_flag] = 0 + new_sat = diff_gdq.astype('uint8') + fits.writeto("diff_gdq.fits", diff_gdq, overwrite=True) + fits.writeto('current_gdq.fits', current_gdq, overwrite = True) + fits.writeto('prev_gdq.fits', prev_gdq, overwrite=True) +# new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] +# fits.writeto("new_flagged_pixels1.fits", new_flagged_pixels, overwrite=True) # find the circle parameters for newly saturated pixels - sat_circles = find_circles(new_flagged_pixels, sat_flag, min_sat_area) + sat_ellipses = find_ellipses(new_sat, sat_flag, min_sat_area) # expand the larger saturated cores to deal with the charge migration from the # saturated cores. # gdq[integration, , :, :] = extend_saturation(gdq[integration, :, :, :], gdq[integration, :, :, :] = extend_saturation(gdq[integration, :, :, :], - group, sat_circles, sat_flag, jump_flag, + group, sat_ellipses, sat_flag, jump_flag, min_sat_radius_extend, expansion=sat_expand) fits.writeto("after_extend_large_events.fits", gdq, overwrite=True) # recalculate the newly flagged pixels after the expansion of saturation - new_flagged_pixels2 = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] - sat_circles2 = find_circles(new_flagged_pixels2, sat_flag, min_sat_area) + current_gdq = 1.0 * gdq[integration, group, :, :] + prev_gdq = 1.0 * gdq[integration, group - 1, :, :] + diff_gdq = 1.0 * current_gdq - prev_gdq + new_sat = diff_gdq.astype('uint8') + fits.writeto("diff_gdq2.fits", diff_gdq, overwrite=True) + fits.writeto('current_gdq2.fits', current_gdq, overwrite=True) + fits.writeto('prev_gdq2.fits', prev_gdq, overwrite=True) # find all the newly saturated pixel - sat_pixels = np.bitwise_and(new_flagged_pixels2, sat_flag) + sat_pixels = np.bitwise_and(diff_gdq.astype('uint8'), sat_flag) saty, satx = np.where(sat_pixels == sat_flag) - only_jump = gdq[integration, group, :, :].copy() + only_jump = diff_gdq.copy() + fits.writeto("onlyjump.fits", only_jump, overwrite=True) # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag + fits.writeto("onlyjump2.fits", only_jump, overwrite=True) # only_jump_cube[integration, group, :, :] = only_jump - jump_ellipses = find_ellipses(only_jump, jump_flag, min_jump_area) + jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) if sat_required_snowball: - snowballs = make_snowballs(jump_ellipses, sat_circles2, group) + snowballs = make_snowballs(jump_ellipses, sat_ellipses, group) else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) @@ -389,6 +406,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, snowballs, sat_flag, jump_flag, expansion=expand_factor) + fits.writeto("final_gdq.fits", gdq[integration, group,:, :], overwrite=True) # fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) if use_ellipses: if np.all(np.array(n_showers_grp_ellipse) == 0): @@ -425,31 +443,37 @@ def extend_snowballs(plane, snowballs, sat_flag, jump_flag, expansion=1.5): return plane, num_circles -def extend_saturation(cube, grp, sat_circles, sat_flag, jump_flag, +def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, min_sat_radius_extend, expansion=1): image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) print("Grp in ES", grp) count = 0 - for sat_circle in sat_circles: - radius = sat_circle[1] + for ellipse in sat_ellipses: + ceny = ellipse[0][0] + cenx = ellipse[0][1] + minor_axis = min(ellipse[1][1], ellipse[1][0]) count = count + 1 - print("Grp", grp, " radius ", radius, "count", count, "center", sat_circle[0]) - if radius > min_sat_radius_extend: - new_radius = round(radius + expansion) - sat_center = sat_circle[0] - cenx = sat_center[1] - ceny = sat_center[0] - image = cv.circle(image, (round(ceny), round(cenx)), new_radius, (0, 0, 4), -1) - sat_circle = image[:, :, 2] - saty, satx = np.where(sat_circle == 4) + print("Grp", grp, " radius ", minor_axis, "count", count, "center", ellipse[0]) + if minor_axis > min_sat_radius_extend: + if ellipse[1][1] < ellipse[1][0]: + axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] + axis2 = ellipse[1][1] * expansion + else: + axis1 = ellipse[1][0] * expansion + axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + alpha = ellipse[2] + image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), + round(axis2 / 2)), alpha, 0, 360, (0, 0, 22), -1) + sat_ellipse = image[:, :, 2] + saty, satx = np.where(sat_ellipse == 22) cube[grp:, saty, satx] = sat_flag return cube -def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1): +def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1, expand_by_ratio=True): # For a given DQ plane it will use the list of ellipses to create expanded ellipses of pixels with # the jump flag set. image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) @@ -472,6 +496,7 @@ def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1): image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), round(axis2 / 2)), alpha, 0, 360, (0, 0, 4), -1) jump_ellipse = image[:, :, 2] + # don't add any jump flags to pixels that are already saturated saty, satx = np.where(sat_pix == sat_flag) jump_ellipse[saty, satx] = 0 plane = np.bitwise_or(plane, jump_ellipse) diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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%0D*l1^*&Lz_hm7Pz#Ia3{=e!b$8E(|^SypdpgJF$ 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 z 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 B7McJ6 literal 0 HcmV?d00001 diff --git a/tests/test_jump.py b/tests/test_jump.py index ecaa9e6e..2e8fc451 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -3,7 +3,7 @@ from astropy.io import fits from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation, \ - point_inside_ellipse, point_inside_rectangle + point_inside_ellipse, point_inside_rectangle, flag_large_events DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1} @@ -134,3 +134,14 @@ def test_inside_ellipse4(): point = (0.5, 1) result = point_inside_rectangle(point, ellipse) assert result + + +def test_plane23(): + incube = fits.getdata('input_jump_cube.fits') + testcube = incube[:, 22:24, :, :] + + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + fits.writeto("output_jump_cube.fits", testcube, overwrite=True) \ No newline at end of file From 8d65179987b5f337758d00a3be82c36f57e00b21 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 15:17:46 -0500 Subject: [PATCH 019/196] fix inside ellipse test --- src/stcal/jump/jump.py | 16 ++++++++++++---- tests/test_jump.py | 25 ++++++++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index c4b6cef8..23adeb23 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -533,22 +533,24 @@ def make_snowballs(jump_ellipses, sat_circles, grp): sat_found = False for sat in sat_circles: # center of saturation is within the enclosing jump rectangle - if point_inside_rectangle(sat[0], jump): + if point_inside_ellipse(sat[0], jump): if jump not in snowballs: snowballs.append(jump) sat_found = True - if not sat_found and grp > 22: + if not sat_found: print("no saturation within jump rectangle ", grp, jump) return snowballs -def point_inside_ellipse(pointy, pointx, ellipse): +def old_point_inside_ellipse(point, ellipse): box = cv.boxPoints(ellipse) ceny = ellipse[0][0] cenx = ellipse[0][1] axis1 = ellipse[1][0] axis2 = ellipse[1][1] theta = np.deg2rad(ellipse[2]) + pointx = point[0] + pointy = point[1] radius = ((np.cos(theta) * (pointx - cenx) + np.sin(theta) * (pointy - ceny))**2)/axis2**2 + \ ((np.sin(theta) * (pointx - cenx) + np.cos(theta) * (pointy - ceny))**2)/axis1**2 if radius < 1: @@ -556,7 +558,13 @@ def point_inside_ellipse(pointy, pointx, ellipse): else: return False - +def point_inside_ellipse(point, ellipse): + delta_center = np.sqrt((point[0]-ellipse[0][0])**2 + (point[1]-ellipse[0][1])**2) + minor_axis = min(ellipse[1][0], ellipse[1][0]) + if delta_center < minor_axis: + return True + else: + return False def point_inside_rectangle(point, ellipse): box = cv.boxPoints(ellipse) area1 = triangle_area(point, box[0], box[1]) diff --git a/tests/test_jump.py b/tests/test_jump.py index 2e8fc451..10768a74 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -125,16 +125,21 @@ def test_inside_ellipse3(): def test_inside_ellipse5(): ellipse = ((0, 0), (1, 2), -10) - point = (0.5, 1) - result = point_inside_rectangle(point, ellipse) + point = (1, 0.6) + result = point_inside_ellipse(point, ellipse) assert not result def test_inside_ellipse4(): ellipse = ((0, 0), (1, 2), 0) - point = (0.5, 1) - result = point_inside_rectangle(point, ellipse) + point = (1, 0.5) + result = point_inside_ellipse(point, ellipse) assert result +def test_inside_ellipes5(): + point = (1110.5, 870.5) + ellipse = ((1111.0001220703125, 870.5000610351562), (10.60660171508789, 10.60660171508789), 45.0) + result = point_inside_ellipse(point, ellipse) + assert result def test_plane23(): incube = fits.getdata('input_jump_cube.fits') @@ -144,4 +149,14 @@ def test_plane23(): min_jump_area=6, expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube.fits", testcube, overwrite=True) \ No newline at end of file + fits.writeto("output_jump_cube23.fits", testcube, overwrite=True) + +def test_plane13(): + incube = fits.getdata('input_jump_cube.fits') + testcube = incube[:, 13:15, :, :] + + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + fits.writeto("output_jump_cube13.fits", testcube, overwrite=True) From ca6df08b356e68c381a8270a60825cc6e02c5a84 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 15:29:05 -0500 Subject: [PATCH 020/196] Update jump.py --- src/stcal/jump/jump.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 23adeb23..b90b7186 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -364,9 +364,9 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, diff_gdq = 1.0 * current_gdq - prev_gdq diff_gdq[diff_gdq != sat_flag] = 0 new_sat = diff_gdq.astype('uint8') - fits.writeto("diff_gdq.fits", diff_gdq, overwrite=True) - fits.writeto('current_gdq.fits', current_gdq, overwrite = True) - fits.writeto('prev_gdq.fits', prev_gdq, overwrite=True) +# fits.writeto("diff_gdq.fits", diff_gdq, overwrite=True) +# fits.writeto('current_gdq.fits', current_gdq, overwrite = True) +# fits.writeto('prev_gdq.fits', prev_gdq, overwrite=True) # new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] # fits.writeto("new_flagged_pixels1.fits", new_flagged_pixels, overwrite=True) # find the circle parameters for newly saturated pixels @@ -377,24 +377,24 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, gdq[integration, :, :, :] = extend_saturation(gdq[integration, :, :, :], group, sat_ellipses, sat_flag, jump_flag, min_sat_radius_extend, expansion=sat_expand) - fits.writeto("after_extend_large_events.fits", gdq, overwrite=True) +# fits.writeto("after_extend_large_events.fits", gdq, overwrite=True) # recalculate the newly flagged pixels after the expansion of saturation current_gdq = 1.0 * gdq[integration, group, :, :] prev_gdq = 1.0 * gdq[integration, group - 1, :, :] diff_gdq = 1.0 * current_gdq - prev_gdq new_sat = diff_gdq.astype('uint8') - fits.writeto("diff_gdq2.fits", diff_gdq, overwrite=True) - fits.writeto('current_gdq2.fits', current_gdq, overwrite=True) - fits.writeto('prev_gdq2.fits', prev_gdq, overwrite=True) +# fits.writeto("diff_gdq2.fits", diff_gdq, overwrite=True) +# fits.writeto('current_gdq2.fits', current_gdq, overwrite=True) +# fits.writeto('prev_gdq2.fits', prev_gdq, overwrite=True) # find all the newly saturated pixel sat_pixels = np.bitwise_and(diff_gdq.astype('uint8'), sat_flag) saty, satx = np.where(sat_pixels == sat_flag) only_jump = diff_gdq.copy() - fits.writeto("onlyjump.fits", only_jump, overwrite=True) +# fits.writeto("onlyjump.fits", only_jump, overwrite=True) # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag - fits.writeto("onlyjump2.fits", only_jump, overwrite=True) +# fits.writeto("onlyjump2.fits", only_jump, overwrite=True) # only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) if sat_required_snowball: @@ -406,7 +406,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, snowballs, sat_flag, jump_flag, expansion=expand_factor) - fits.writeto("final_gdq.fits", gdq[integration, group,:, :], overwrite=True) +# fits.writeto("final_gdq.fits", gdq[integration, group,:, :], overwrite=True) # fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) if use_ellipses: if np.all(np.array(n_showers_grp_ellipse) == 0): @@ -447,14 +447,14 @@ def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, min_sat_radius_extend, expansion=1): image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) - print("Grp in ES", grp) +# print("Grp in ES", grp) count = 0 for ellipse in sat_ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] minor_axis = min(ellipse[1][1], ellipse[1][0]) count = count + 1 - print("Grp", grp, " radius ", minor_axis, "count", count, "center", ellipse[0]) +# print("Grp", grp, " radius ", minor_axis, "count", count, "center", ellipse[0]) if minor_axis > min_sat_radius_extend: if ellipse[1][1] < ellipse[1][0]: axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] @@ -538,7 +538,7 @@ def make_snowballs(jump_ellipses, sat_circles, grp): snowballs.append(jump) sat_found = True if not sat_found: - print("no saturation within jump rectangle ", grp, jump) + # print("no saturation within jump rectangle ", grp, jump) return snowballs From c7f6c5f19e6a0ffc28b5f59707efd8c2bd1ae3f8 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 15:30:58 -0500 Subject: [PATCH 021/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index b90b7186..13be457c 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -537,7 +537,7 @@ def make_snowballs(jump_ellipses, sat_circles, grp): if jump not in snowballs: snowballs.append(jump) sat_found = True - if not sat_found: + # if not sat_found: # print("no saturation within jump rectangle ", grp, jump) return snowballs From e907be05cd81657020120a604f6c40307bc3ed69 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 6 Jan 2023 16:57:09 -0500 Subject: [PATCH 022/196] Update jump.py --- src/stcal/jump/jump.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 13be457c..b0bb7830 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -345,10 +345,13 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] n_showers_grp_ellipse = [] -# fits.writeto("input_jump_cube.fits", gdq, overwrite=True) + fits.writeto("input_jump_cube.fits", gdq, overwrite=True) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): - print("Group ",group) + if (group//10) * 10 == group: + print("Group ", group) + else: + print("Group ", group, end=" ") if use_ellipses: new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] jump_ellipses = find_ellipses(new_flagged_pixels, jump_flag, min_jump_area) From 88529676118be041d2a78f6e94d9d7e876f855d9 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 11 Jan 2023 10:52:08 -0500 Subject: [PATCH 023/196] tests etc --- .DS_Store | Bin 0 -> 6148 bytes src/stcal/jump/jump.py | 4 ++-- tests/test_jump.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5873551b20e159e39c741a62b52a32beec4288bc GIT binary patch literal 6148 zcmeHKF=_)r43uIA3~5}t+%Mz@i*a6%4}{q142GQ3UzK;|X`YcpusIIgqzNOCW_M27 z<)%2D%*?l6hi9|3nXTYN`(~ILpVKGyPz=#|#{T$zIvugJ$4T-G!PLv7PX|Jb0Kg^a zFsx&i05%f9UN|O#fq9k!v(#(E@GJ-4Dz6ugiCJzQH{+bT*=s`axE*|pbn~95C Date: Thu, 12 Jan 2023 11:12:15 -0500 Subject: [PATCH 024/196] return new dgq for sat expend --- src/stcal/jump/jump.py | 15 +++++++++------ tests/test_jump.py | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index e0d9aaaa..7efaea15 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -371,8 +371,8 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # fits.writeto('current_gdq.fits', current_gdq, overwrite = True) # fits.writeto('prev_gdq.fits', prev_gdq, overwrite=True) # new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] -# fits.writeto("new_flagged_pixels1.fits", new_flagged_pixels, overwrite=True) - # find the circle parameters for newly saturated pixels + fits.writeto("new_sat.fits", new_sat, overwrite=True) + # find the ellipse parameters for newly saturated pixels sat_ellipses = find_ellipses(new_sat, sat_flag, min_sat_area) # expand the larger saturated cores to deal with the charge migration from the # saturated cores. @@ -397,7 +397,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag -# fits.writeto("onlyjump2.fits", only_jump, overwrite=True) + fits.writeto("onlyjump2.fits", only_jump, overwrite=True) # only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) if sat_required_snowball: @@ -452,6 +452,7 @@ def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) # print("Grp in ES", grp) count = 0 + outcube = cube.copy() for ellipse in sat_ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] @@ -470,8 +471,8 @@ def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, round(axis2 / 2)), alpha, 0, 360, (0, 0, 22), -1) sat_ellipse = image[:, :, 2] saty, satx = np.where(sat_ellipse == 22) - cube[grp:, saty, satx] = sat_flag - return cube + outcube[grp:, saty, satx] = sat_flag + return outcube @@ -539,6 +540,7 @@ def make_snowballs(jump_ellipses, sat_circles, grp): if point_inside_ellipse(sat[0], jump): if jump not in snowballs: snowballs.append(jump) + print("sat inside found", sat, jump) sat_found = True # if not sat_found: # print("no saturation within jump rectangle ", grp, jump) @@ -561,9 +563,10 @@ def old_point_inside_ellipse(point, ellipse): else: return False + def point_inside_ellipse(point, ellipse): delta_center = np.sqrt((point[0]-ellipse[0][0])**2 + (point[1]-ellipse[0][1])**2) - minor_axis = min(ellipse[1][0], ellipse[1][0]) + minor_axis = min(ellipse[1][0], ellipse[1][1]) if delta_center < minor_axis: return True else: diff --git a/tests/test_jump.py b/tests/test_jump.py index 2fe54c84..b9bf10ba 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -172,12 +172,22 @@ def test_5580_plane8(): sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) fits.writeto("output_jump_cube8.fits", testcube, overwrite=True) -def test_1035_plane3(): - incube = fits.getdata('input1035_jump_cube.fits') - testcube = incube[:, 1:3, :, :] +def test_2333_plane25(): + incube = fits.getdata('input_jump_cube23-33.fits') + testcube = incube[:, 0:3, :, :] flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, min_jump_area=6, expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube8.fits", testcube, overwrite=True) + fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) + + +def test_inputjumpall(): + testcube = fits.getdata('input_jump_cube.fits') + + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) From 92801a450341a51c2383b175873c064aa96bcec6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 12 Jan 2023 12:22:53 -0500 Subject: [PATCH 025/196] fix super expanding ellipses --- src/stcal/jump/jump.py | 3 ++- tests/test_jump.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 7efaea15..ca1f39e5 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -385,6 +385,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, current_gdq = 1.0 * gdq[integration, group, :, :] prev_gdq = 1.0 * gdq[integration, group - 1, :, :] diff_gdq = 1.0 * current_gdq - prev_gdq + diff_gdq[diff_gdq < 0] = 0 new_sat = diff_gdq.astype('uint8') # fits.writeto("diff_gdq2.fits", diff_gdq, overwrite=True) # fits.writeto('current_gdq2.fits', current_gdq, overwrite=True) @@ -410,7 +411,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, jump_flag, expansion=expand_factor) # fits.writeto("final_gdq.fits", gdq[integration, group,:, :], overwrite=True) -# fits.writeto("only_jump_cube.fits", only_jump_cube, overwrite=True) + fits.writeto("last_gdq_inside.fits", gdq, overwrite=True) if use_ellipses: if np.all(np.array(n_showers_grp_ellipse) == 0): log.info(f'No showers found in integration {integration}.') diff --git a/tests/test_jump.py b/tests/test_jump.py index b9bf10ba..91254665 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -3,9 +3,9 @@ from astropy.io import fits from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation, \ - point_inside_ellipse, point_inside_rectangle, flag_large_events + point_inside_ellipse, point_inside_rectangle, flag_large_events, detect_jumps -DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1} +DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1, 'GOOD': 0, 'NO_GAIN_VALUE': 8} try: import cv2 as cv # noqa: F401 @@ -177,8 +177,8 @@ def test_2333_plane25(): testcube = incube[:, 0:3, :, :] flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, + min_jump_area=15, + expand_factor=2.5, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) @@ -191,3 +191,29 @@ def test_inputjumpall(): expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) + + +def test_detect_jumps_runaway(): + testcube = fits.getdata('smalldark2232_00_dark_current.fits') + hdl = fits.open('smalldark2232_00_dark_current.fits') + gdq = hdl['GROUPDQ'].data + pdq = hdl['pixeldq'].data + err = np.ones_like(pdq).astype('float64') + gain_2d = fits.getdata('jwst_nirspec_gain_0023.fits') + readnoise_2d = fits.getdata('jwst_nirspec_readnoise_0038.fits') + + detect_jumps(1, testcube, gdq, pdq, err, + gain_2d, readnoise_2d, 4, + 5, 6, 'half', 1000, + 10, True, DQFLAGS, + after_jump_flag_dn1=0.0, + after_jump_flag_n1=0, + after_jump_flag_dn2=0.0, + after_jump_flag_n2=0, + min_sat_area=1, + min_jump_area=15, + expand_factor=2.5, + use_ellipses=False, + sat_required_snowball=True, + expand_large_events=True) + fits.writeto("output_gdq.fits", gdq, overwrite=True) From 2b3b1de2bf3b73c514395c4872ed68cf640280f7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 13 Jan 2023 11:57:23 -0500 Subject: [PATCH 026/196] Update jump.py --- src/stcal/jump/jump.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index ca1f39e5..8b2f086c 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -35,7 +35,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, - expand_large_events=True): + expand_large_events=True, + edge_size = 15): """ This is the high-level controlling routine for the jump detection process. It loads and sets the various input data and parameters needed by each of @@ -303,7 +304,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, min_jump_area=min_jump_area, expand_factor=expand_factor, use_ellipses=use_ellipses, - sat_required_snowball=sat_required_snowball) + sat_required_snowball=sat_required_snowball, + edge_size=edge_size) elapsed = time.time() - start log.info('Total elapsed time = %g sec' % elapsed) @@ -321,7 +323,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2): + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2, + edge_size=15): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called snowballs for the NIR and are almost always circular with a saturated core. For MIRI they are better @@ -402,7 +405,9 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, # only_jump_cube[integration, group, :, :] = only_jump jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) if sat_required_snowball: - snowballs = make_snowballs(jump_ellipses, sat_ellipses, group) + low_threshold = edge_size + high_threshold = gdq.shape[0] - edge_size + snowballs = make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold) else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) @@ -530,19 +535,22 @@ def find_ellipses(dqplane, bitmask, min_area): return ellipses -def make_snowballs(jump_ellipses, sat_circles, grp): +def make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold): # Ths routine will create a list of snowballs (ellipses) that have the center of the saturation circle # within the enclosing jump rectangle. snowballs = [] for jump in jump_ellipses: sat_found = False - for sat in sat_circles: - # center of saturation is within the enclosing jump rectangle - if point_inside_ellipse(sat[0], jump): - if jump not in snowballs: - snowballs.append(jump) - print("sat inside found", sat, jump) - sat_found = True + if near_edge(jump, low_threshold, high_threshold): + snowballs.append(jump) + else: + for sat in sat_ellipses: + # center of saturation is within the enclosing jump rectangle + if point_inside_ellipse(sat[0], jump): + if jump not in snowballs: + snowballs.append(jump) + print("sat inside found", sat, jump) + sat_found = True # if not sat_found: # print("no saturation within jump rectangle ", grp, jump) return snowballs @@ -594,3 +602,10 @@ def triangle_area(point, vert1, vert2): (point[1] * vert2[0] - vert2[1] * point[0])) / 2 return area +def near_edge(jump, low_threshold, high_threshold): + if jump[0][0] < low_threshold or jump[0][1] < low_threshold: + return True + elif jump[0][0] > high_threshold or jump[0][1] > high_threshold: + return True + else: + return False \ No newline at end of file From 42350f20ef3a07b526aa773629060dfb8d05a01c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 13 Jan 2023 11:59:06 -0500 Subject: [PATCH 027/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 8b2f086c..0a146943 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -549,7 +549,7 @@ def make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold): if point_inside_ellipse(sat[0], jump): if jump not in snowballs: snowballs.append(jump) - print("sat inside found", sat, jump) +# print("sat inside found", sat, jump) sat_found = True # if not sat_found: # print("no saturation within jump rectangle ", grp, jump) From 82f70344af586d45b7be527ec122157b194c6c72 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 13 Jan 2023 14:49:44 -0500 Subject: [PATCH 028/196] wrong shape for edge too many expands due to edge being wrong --- src/stcal/jump/jump.py | 7 +++---- tests/test_jump.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 0a146943..d33919bc 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -406,7 +406,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) if sat_required_snowball: low_threshold = edge_size - high_threshold = gdq.shape[0] - edge_size + high_threshold = gdq.shape[2] - edge_size snowballs = make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold) else: snowballs = jump_ellipses @@ -603,9 +603,8 @@ def triangle_area(point, vert1, vert2): return area def near_edge(jump, low_threshold, high_threshold): - if jump[0][0] < low_threshold or jump[0][1] < low_threshold: - return True - elif jump[0][0] > high_threshold or jump[0][1] > high_threshold: + if jump[0][0] < low_threshold or jump[0][1] < low_threshold\ + or jump[0][0] > high_threshold or jump[0][1] > high_threshold: return True else: return False \ No newline at end of file diff --git a/tests/test_jump.py b/tests/test_jump.py index 91254665..f642a74c 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -177,7 +177,7 @@ def test_2333_plane25(): testcube = incube[:, 0:3, :, :] flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=15, + min_jump_area=7, expand_factor=2.5, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) @@ -211,7 +211,7 @@ def test_detect_jumps_runaway(): after_jump_flag_dn2=0.0, after_jump_flag_n2=0, min_sat_area=1, - min_jump_area=15, + min_jump_area=5, expand_factor=2.5, use_ellipses=False, sat_required_snowball=True, From a9b0958712f55716b5f04a7a4368eeddd99fd8f2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 13 Jan 2023 16:29:10 -0500 Subject: [PATCH 029/196] update edge parameter --- src/stcal/jump/jump.py | 8 ++++---- tests/test_jump.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index d33919bc..655559ce 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -36,7 +36,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, use_ellipses=False, sat_required_snowball=True, expand_large_events=True, - edge_size = 15): + edge_size = 25): """ This is the high-level controlling routine for the jump detection process. It loads and sets the various input data and parameters needed by each of @@ -324,7 +324,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, min_jump_area=6, expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2, - edge_size=15): + edge_size=25): """ This routine controls the creation of expanded regions that are flagged as jumps. These are called snowballs for the NIR and are almost always circular with a saturated core. For MIRI they are better @@ -551,8 +551,8 @@ def make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold): snowballs.append(jump) # print("sat inside found", sat, jump) sat_found = True - # if not sat_found: - # print("no saturation within jump rectangle ", grp, jump) + if not sat_found: + print("no saturation within jump rectangle ", jump) return snowballs diff --git a/tests/test_jump.py b/tests/test_jump.py index f642a74c..c930e2c5 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -182,6 +182,17 @@ def test_2333_plane25(): sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) +def test_edgeflage_130140(): + incube = fits.getdata('input_jump_cube130140.fits') + testcube = incube[:, 3:5, :, :] + + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=7, + expand_factor=2.5, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) + + def test_inputjumpall(): testcube = fits.getdata('input_jump_cube.fits') From 1e7ae8e06f8eb1d1f7c891e27b0b31cbc9d61c6b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 17 Jan 2023 20:07:46 -0500 Subject: [PATCH 030/196] remore print --- src/stcal/jump/jump.py | 42 +++++++++++++++++++----------------------- tests/test_jump.py | 4 ++-- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 655559ce..3b81e92e 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -362,8 +362,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, gdq[integration, group, :, :], num_events = \ extend_ellipses(gdq[integration, group, :, :], jump_ellipses, sat_flag, jump_flag, -############# is sat_expand the right parameter?????????????????? ########################### - expansion=sat_expand) + expansion=expand_factor) else: current_gdq = 1.0 * gdq[integration, group, :, :] prev_gdq = 1.0 * gdq[integration, group - 1, :, :] @@ -397,7 +396,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, sat_pixels = np.bitwise_and(diff_gdq.astype('uint8'), sat_flag) saty, satx = np.where(sat_pixels == sat_flag) only_jump = diff_gdq.copy() -# fits.writeto("onlyjump.fits", only_jump, overwrite=True) + fits.writeto("onlyjump.fits", only_jump, overwrite=True) # reset the saturated pixel to be jump to allow the jump circles to have the # central saturated region set to "jump" instead of "saturation". only_jump[saty, satx] = jump_flag @@ -453,28 +452,21 @@ def extend_snowballs(plane, snowballs, sat_flag, jump_flag, expansion=1.5): def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, - min_sat_radius_extend, expansion=1): + min_sat_radius_extend, expansion=2): image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) -# print("Grp in ES", grp) - count = 0 outcube = cube.copy() for ellipse in sat_ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] minor_axis = min(ellipse[1][1], ellipse[1][0]) - count = count + 1 # print("Grp", grp, " radius ", minor_axis, "count", count, "center", ellipse[0]) if minor_axis > min_sat_radius_extend: - if ellipse[1][1] < ellipse[1][0]: - axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] - axis2 = ellipse[1][1] * expansion - else: - axis1 = ellipse[1][0] * expansion - axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + axis1 = ellipse[1][0] + expansion + axis2 = ellipse[1][1] + expansion alpha = ellipse[2] - image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), - round(axis2 / 2)), alpha, 0, 360, (0, 0, 22), -1) + image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 + 0.5), + round(axis2 + 0.5)), alpha, 0, 360, (0, 0, 22), -1) sat_ellipse = image[:, :, 2] saty, satx = np.where(sat_ellipse == 22) outcube[grp:, saty, satx] = sat_flag @@ -483,7 +475,7 @@ def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, -def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1, expand_by_ratio=True): +def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.9, expand_by_ratio=True): # For a given DQ plane it will use the list of ellipses to create expanded ellipses of pixels with # the jump flag set. image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) @@ -496,12 +488,16 @@ def extend_ellipses(plane, ellipses, sat_flag, jump_flag, expansion=1.1, expand_ # the number of pixels added to the minor axis. This prevents very large flagged ellipses # with high axis ratio ellipses. The major and minor axis are not always the same index. # Therefore, we have to test to find which is actually the minor axis. - if ellipse[1][1] < ellipse[1][0]: - axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] - axis2 = ellipse[1][1] * expansion + if expand_by_ratio: + if ellipse[1][1] < ellipse[1][0]: + axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] + axis2 = ellipse[1][1] * expansion + else: + axis1 = ellipse[1][0] * expansion + axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] else: - axis1 = ellipse[1][0] * expansion - axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + axis1 = ellipse[1][0] + expansion + axis2 = ellipse[1][1] + expansion alpha = ellipse[2] image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), round(axis2 / 2)), alpha, 0, 360, (0, 0, 4), -1) @@ -551,8 +547,8 @@ def make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold): snowballs.append(jump) # print("sat inside found", sat, jump) sat_found = True - if not sat_found: - print("no saturation within jump rectangle ", jump) +# if not sat_found: +# print("no saturation within jump rectangle ", jump) return snowballs diff --git a/tests/test_jump.py b/tests/test_jump.py index c930e2c5..f4fb506b 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -184,12 +184,12 @@ def test_2333_plane25(): def test_edgeflage_130140(): incube = fits.getdata('input_jump_cube130140.fits') - testcube = incube[:, 3:5, :, :] + testcube = incube[:, :-1, :, :] flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, min_jump_area=7, expand_factor=2.5, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=3) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) From f3cea06b1c3fed20e2697ef8d2db923441ee693e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 18 Jan 2023 13:20:02 -0500 Subject: [PATCH 031/196] Update saturation.py --- src/stcal/saturation/saturation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stcal/saturation/saturation.py b/src/stcal/saturation/saturation.py index 45f92f58..84b0b376 100644 --- a/src/stcal/saturation/saturation.py +++ b/src/stcal/saturation/saturation.py @@ -61,8 +61,6 @@ def flag_saturated_pixels( """ nints, ngroups, nrows, ncols = data.shape - print("local version of saturation") - stop saturated = dqflags['SATURATED'] ad_floor = dqflags['AD_FLOOR'] no_sat_check = dqflags['NO_SAT_CHECK'] From 312b21f1c463e5eb1f390d84edbeb7fde293e941 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 18 Jan 2023 15:01:21 -0500 Subject: [PATCH 032/196] fix miri showers --- src/stcal/jump/jump.py | 6 ++++-- tests/test_jump.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 3b81e92e..5cdd9315 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -356,8 +356,10 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, else: print("Grp", group, end=" ") if use_ellipses: - new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] - jump_ellipses = find_ellipses(new_flagged_pixels, jump_flag, min_jump_area) + new_flagged_pixels = 1.0*gdq[integration, group, :, :] - 1.0*gdq[integration, group - 1, :, :] + new_flagged_pixels[new_flagged_pixels < 0] = 0 + fits.writeto('new_flagged_pixels.fits', new_flagged_pixels, overwrite=True) + jump_ellipses = find_ellipses(new_flagged_pixels.astype('uint8'), jump_flag, min_jump_area) n_showers_grp_ellipse.append(len(jump_ellipses)) gdq[integration, group, :, :], num_events = \ extend_ellipses(gdq[integration, group, :, :], diff --git a/tests/test_jump.py b/tests/test_jump.py index f4fb506b..31716546 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -192,7 +192,16 @@ def test_edgeflage_130140(): sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=3) fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) +def test_miri_input(): + incube = fits.getdata('input_jump_cube_miri_01.fits') + testcube = incube[:, 1:5, :, :] + testcube = incube + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=7, + expand_factor=2.5, use_ellipses=True, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=3) + fits.writeto("output_jump_cube_miri.fits", testcube, overwrite=True) def test_inputjumpall(): testcube = fits.getdata('input_jump_cube.fits') From e309f334948139bc611deea47e8dea309d9ecd79 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 12:55:14 -0500 Subject: [PATCH 033/196] updates --- src/stcal/jump/twopoint_difference.py | 4 ++-- tests/test_jump.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 1d1c7525..259059f4 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -120,8 +120,8 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, jump_flag = dqflags["JUMP_DET"] for integ in range(nints): - - log.info(f'Working on integration {integ + 1}:') + if integ//100 * 100 == integ: + log.info(f'Working on integration {integ + 1}:') # get data, gdq for this integration dat = dataa[integ] diff --git a/tests/test_jump.py b/tests/test_jump.py index 31716546..754dfaeb 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -60,6 +60,16 @@ def test_find_simple_ellipse(): assert ellipse[0][0] == pytest.approx((2.5, 2.0)) # center +def test_find_ellipse2(): + plane = np.zeros(shape=(5, 5), dtype=np.uint8) + plane[1,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'],DQFLAGS['JUMP_DET'], 0] + plane[2,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] + plane[3,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] + ellipse = find_ellipses(plane, DQFLAGS['JUMP_DET'], 1) + print(ellipse) + assert ellipse == 1 + + def test_extend_saturation_simple(): cube = np.zeros(shape=(5, 7, 7), dtype=np.uint8) grp = 1 From ee0051b899f3f4d8cd77b7c4492a1a2417d6fc73 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 12:59:16 -0500 Subject: [PATCH 034/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 259059f4..9923c604 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -162,9 +162,9 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, max_ratio > three_diff_rej_thresh)) row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh)) - - log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' - log.info(log_str.format(len(row4cr), 'five or more')) + if integ // 100 * 100 == integ: + log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' + log.info(log_str.format(len(row4cr), 'five or more')) # get the rows, col pairs for all pixels with at least one CR all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) @@ -288,7 +288,8 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") + if integ // 100 * 100 == integ: + log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") for j in range(len(cr_group)): group = cr_group[j] From 8fcbb282049881505102832d01ed067693881849 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 13:53:37 -0500 Subject: [PATCH 035/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 9923c604..c65555e1 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -120,8 +120,7 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, jump_flag = dqflags["JUMP_DET"] for integ in range(nints): - if integ//100 * 100 == integ: - log.info(f'Working on integration {integ + 1}:') + log.info(f'Working on integration {integ + 1}:') # get data, gdq for this integration dat = dataa[integ] @@ -162,9 +161,8 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, max_ratio > three_diff_rej_thresh)) row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh)) - if integ // 100 * 100 == integ: - log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' - log.info(log_str.format(len(row4cr), 'five or more')) + log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' + log.info(log_str.format(len(row4cr), 'five or more')) # get the rows, col pairs for all pixels with at least one CR all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) @@ -288,8 +286,7 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - if integ // 100 * 100 == integ: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") + log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") for j in range(len(cr_group)): group = cr_group[j] From f4b995a74fb5de62178351349f658b65555fa219 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 15:15:54 -0500 Subject: [PATCH 036/196] retro fit sigman_clip --- src/stcal/jump/twopoint_difference.py | 158 ++++++++++---------------- 1 file changed, 61 insertions(+), 97 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index c65555e1..056a1720 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -1,14 +1,15 @@ import logging import numpy as np - +import astropy.stats as stats log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -def find_crs(dataa, group_dq, read_noise, rejection_thresh, +def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, two_diff_rej_thresh, three_diff_rej_thresh, nframes, flag_4_neighbors, max_jump_to_flag_neighbors, min_jump_to_flag_neighbors, dqflags, + minimum_groups=3, minimum_selfcal_groups=100, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, @@ -119,24 +120,33 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, dnu_flag = dqflags["DO_NOT_USE"] jump_flag = dqflags["JUMP_DET"] - for integ in range(nints): - log.info(f'Working on integration {integ + 1}:') - - # get data, gdq for this integration - dat = dataa[integ] - gdq_integ = gdq[integ] + # get data, gdq for this integration + dat = dataa + num_flagged_grps = 0 + # determine the number of groups with all pixels set to DO_NOT_USE + for grp in range(dat.shape[1]): + if np.all(np.bitwise_and(gdq[0, grp, :, :], dnu_flag)): + num_flagged_grps += 1 + total_groups = dat.shape[0] * (dat.shape[1] - num_flagged_grps) + print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", + minimum_selfcal_groups) + if total_groups < minimum_groups: + log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") + return gdq, row_below_gdq, row_above_gdq + else: # set 'saturated' or 'do not use' pixels to nan in data - dat[np.where(np.bitwise_and(gdq_integ, sat_flag))] = np.nan - dat[np.where(np.bitwise_and(gdq_integ, dnu_flag))] = np.nan + # set 'saturated' or 'do not use' pixels to nan in data + dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan + dat[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dat, axis=0) + first_diffs = np.diff(dat, axis=1) # calc. the median of first_diffs for each pixel along the group axis - median_diffs = calc_med_first_diffs(first_diffs) - + first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) + median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -147,93 +157,46 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] - - # create a 2d array containing the value of the largest 'ratio' for each group - max_ratio = np.nanmax(ratio, axis=0) - - # now see if the largest ratio of all groups for each pixel exceeds the threshold. - # there are different threshold for 4+, 3, and 2 usable groups - num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) - row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, - max_ratio > rejection_thresh)) - row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, - max_ratio > three_diff_rej_thresh)) - row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, - max_ratio > two_diff_rej_thresh)) - log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' - log.info(log_str.format(len(row4cr), 'five or more')) - - # get the rows, col pairs for all pixels with at least one CR - all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) - all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) - - # iterate over all groups of the pix w/ an inital CR to look for subsequent CRs - # flag and clip the first CR found. recompute median/sigma/ratio - # and repeat the above steps of comparing the max 'ratio' for each pixel - # to the threshold to determine if another CR can be flagged and clipped. - # repeat this process until no more CRs are found. - for j in range(len(all_crs_row)): - - # get arrays of abs(diffs), ratio, readnoise for this pixel - pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] - pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] - pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] - - # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR - pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) - - # set the largest ratio as a CR - pix_cr_mask[np.nanargmax(pix_ratio)] = 0 - new_CR_found = True - - # loop and check for more CRs, setting the mask as you go and - # clipping the group with the CR. stop when no more CRs are found - # or there is only one two diffs left (which means there is - # actually one left, since the next CR will be masked after - # checking that condition) - - while new_CR_found and ((ndiffs - np.sum(np.isnan(pix_first_diffs))) > 2): - - new_CR_found = False - - # set CRs to nans in first diffs to clip them - pix_first_diffs[~pix_cr_mask] = np.nan - - # recalculate median, sigma, and ratio - new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) - - new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) - new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma - - # check if largest ratio exceeds threhold appropriate for num remaining groups - - # select appropriate thresh. based on number of remaining groups - rej_thresh = rejection_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: - rej_thresh = three_diff_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: - rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio - if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: - new_CR_found = True - pix_cr_mask[new_pix_max_ratio_idx] = 0 - - # Found all CRs for this pix - set flags in input DQ array - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ - np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], - dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) + ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] + + if total_groups >= minimum_selfcal_groups: + log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( + str(total_groups), str(minimum_selfcal_groups))) + clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, + axis=1, masked=True) + max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) + min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) + delta_diff = max_diffs - min_diffs + rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) + avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) + out_avg = avg_diff.filled(fill_value=np.nan) + out_rms = rms_diff.filled(fill_value=np.nan) + out_diffs = delta_diff.filled(fill_value=np.nan) + jump_mask = clipped_diffs.mask + else: + if ndiffs >= 4: + masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) + if ndiffs == 3: + masked_ratio = np.ma.masked_greater(ratio, three_diff_rej_thresh) + if ndiffs == 2: + ratio = (first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] + masked_ratio = np.ma.masked_greater(ratio, two_diff_rej_thresh) + jump_mask = masked_ratio.mask + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False + gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) if flag_4_neighbors: # iterate over each 'jump' pixel - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) for j in range(len(cr_group)): - ratio_this_pix = ratio[cr_group[j] - 1, cr_row[j], cr_col[j]] + ratio_this_pix = ratio[cr_integ[j], cr_group[j] - 1, cr_row[j], cr_col[j]] # Jumps must be in a certain range to have neighbors flagged if ratio_this_pix < max_jump_to_flag_neighbors and \ ratio_this_pix > min_jump_to_flag_neighbors: + integ = cr_integ[j] group = cr_group[j] row = cr_row[j] col = cr_col[j] @@ -280,10 +243,11 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, # flag n groups after jumps above the specified thresholds to account for # the transient seen after ramp jumps - flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] - flag_groups = [after_jump_flag_n1, after_jump_flag_n2] - - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + for integ in range(nints): + flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] + flag_groups = [after_jump_flag_n1, after_jump_flag_n2] + e_jump_int = e_jump[integ, :, :, :] + cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") @@ -292,7 +256,7 @@ def find_crs(dataa, group_dq, read_noise, rejection_thresh, group = cr_group[j] row = cr_row[j] col = cr_col[j] - if e_jump[group - 1, row, col] >= cthres[row, col]: + if e_jump_int[group - 1, row, col] >= cthres[row, col]: for kk in range(group, min(group + cgroup + 1, ngroups)): if (gdq[integ, kk, row, col] & sat_flag) == 0: if (gdq[integ, kk, row, col] & dnu_flag) == 0: @@ -306,7 +270,7 @@ def calc_med_first_diffs(first_diffs): """ Calculate the median of `first diffs` along the group axis. - If there 4+ usable groups (e.g not flagged as saturated, donotuse, + If there are 4+ usable groups (e.g not flagged as saturated, donotuse, or a previously clipped CR), then the group with largest absoulte first difference will be clipped and the median of the remianing groups will be returned. If there are exactly 3 usable groups, the median of From 3b06b548587d54ae3db725cc9c1ea0463c16142c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 16:10:34 -0500 Subject: [PATCH 037/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 056a1720..97b266fc 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -96,7 +96,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pixels above current row also to be flagged as a CR """ - + print("min groups ", minimum_groups ) # copy data and group DQ array if copy_arrs: dataa = dataa.copy() From 9b1414092bc320b62226f203bbb3157f2862b3ac Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 20 Jan 2023 16:15:39 -0500 Subject: [PATCH 038/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 97b266fc..300f8c57 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -9,12 +9,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, two_diff_rej_thresh, three_diff_rej_thresh, nframes, flag_4_neighbors, max_jump_to_flag_neighbors, min_jump_to_flag_neighbors, dqflags, - minimum_groups=3, minimum_selfcal_groups=100, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True): + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=100,): """ Find CRs/Jumps in each integration within the input data array. The input From 1c5708ff665910b1cc0d0cbd4c2dc31739705f59 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 12:57:21 -0500 Subject: [PATCH 039/196] updates for test --- setup.cfg | 0 src/stcal/jump/twopoint_difference.py | 35 +++++++++++++++------------ tests/test_twopoint_difference.py | 20 +++++++++++++++ 3 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..e69de29b diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 300f8c57..b1976b2b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -123,10 +123,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, dat = dataa num_flagged_grps = 0 # determine the number of groups with all pixels set to DO_NOT_USE - for grp in range(dat.shape[1]): - if np.all(np.bitwise_and(gdq[0, grp, :, :], dnu_flag)): - num_flagged_grps += 1 - total_groups = dat.shape[0] * (dat.shape[1] - num_flagged_grps) + for integ in range(dat.shape[0]): + for grp in range(dat.shape[1]): + if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): + gdqvalue = gdq[integ, grp, 0, 0] + num_flagged_grps += 1 + total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", minimum_selfcal_groups) if total_groups < minimum_groups: @@ -145,18 +147,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) - # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - - # reset sigma so pxels with 0 readnoise are not flagged as jumps - sigma[np.where(sigma == 0.)] = np.nan - - # compute 'ratio' for each group. this is the value that will be - # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] if total_groups >= minimum_selfcal_groups: log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( @@ -173,6 +163,19 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask else: + median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) + # calculate sigma for each pixel + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + + # reset sigma so pxels with 0 readnoise are not flagged as jumps + sigma[np.where(sigma == 0.)] = np.nan + + # compute 'ratio' for each group. this is the value that will be + # compared to 'threshold' to classify jumps. subtract the median of + # first_diffs from first_diffs, take the abs. value and divide by sigma. + e_jump = first_diffs - median_diffs[np.newaxis, :, :] + ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] + if ndiffs >= 4: masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) if ndiffs == 3: diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e41b3c7e..e81547b6 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1,5 +1,6 @@ import pytest import numpy as np +from astropy.io import fits from stcal.jump.twopoint_difference import find_crs, calc_med_first_diffs @@ -1021,3 +1022,22 @@ def test_median_func(): arr = np.zeros(4 * 2 * 2).reshape(4, 2, 2) arr[:, 0, 0] = np.array([-1., -2., np.nan, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == -1 + +def test_sigma_clip(): + hdul = fits.open('TSOjump_sc__linearity.fits') + data = hdul['SCI'].data + gdq = hdul['GROUPDQ'].data + indata = np.reshape(data[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) + ingdq = np.reshape(gdq[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) + read_noise = np.ones_like(indata) * 5.9 * 4.0 + gain = np.ones_like(indata) * 4.0 + hdul.close() + find_crs(indata, ingdq, read_noise, 3, + 4, 5, 1, + True, 1000, + 10, DQFLAGS, + after_jump_flag_e1=0.0, + after_jump_flag_n1=0, + after_jump_flag_e2=0.0, + after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=100,) \ No newline at end of file From 8f23ecde2091a552849306bff88406861534f46c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 13:03:53 -0500 Subject: [PATCH 040/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index b1976b2b..c1706469 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -147,6 +147,18 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) + median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) + # calculate sigma for each pixel + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + + # reset sigma so pxels with 0 readnoise are not flagged as jumps + sigma[np.where(sigma == 0.)] = np.nan + + # compute 'ratio' for each group. this is the value that will be + # compared to 'threshold' to classify jumps. subtract the median of + # first_diffs from first_diffs, take the abs. value and divide by sigma. + e_jump = first_diffs - median_diffs[np.newaxis, :, :] + ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] if total_groups >= minimum_selfcal_groups: log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( @@ -163,18 +175,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask else: - median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) - # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - - # reset sigma so pxels with 0 readnoise are not flagged as jumps - sigma[np.where(sigma == 0.)] = np.nan - - # compute 'ratio' for each group. this is the value that will be - # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] if ndiffs >= 4: masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) From b9312887671cb708c7bb7e44309d15222f69aee5 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 15:29:46 -0500 Subject: [PATCH 041/196] fix multint --- src/stcal/jump/twopoint_difference.py | 16 +++++++++++----- tests/test_twopoint_difference.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index c1706469..1c34b68b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -3,7 +3,7 @@ import astropy.stats as stats log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) - +from astropy.io import fits def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, two_diff_rej_thresh, three_diff_rej_thresh, nframes, @@ -102,7 +102,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq = group_dq.copy() else: gdq = group_dq - + fits.writeto("gdq_in.fits", gdq, overwrite=True) # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape ndiffs = ngroups - 1 @@ -126,7 +126,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): - gdqvalue = gdq[integ, grp, 0, 0] num_flagged_grps += 1 total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", @@ -158,9 +157,14 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] +# if nints > 1: + +# else: + if total_groups >= minimum_selfcal_groups: + ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ + sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( str(total_groups), str(minimum_selfcal_groups))) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, @@ -174,8 +178,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask +# print(jump_mask[0:300,:, 0, 0]) else: - + ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ + sigma[np.newaxis, :, :] if ndiffs >= 4: masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) if ndiffs == 3: diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e81547b6..aebacca8 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1029,7 +1029,7 @@ def test_sigma_clip(): gdq = hdul['GROUPDQ'].data indata = np.reshape(data[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) ingdq = np.reshape(gdq[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) - read_noise = np.ones_like(indata) * 5.9 * 4.0 + read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() find_crs(indata, ingdq, read_noise, 3, From f4e4cd25fed2a846badd0b2fee32e7f4c28d5f24 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 16:42:01 -0500 Subject: [PATCH 042/196] tests --- src/stcal/jump/twopoint_difference.py | 9 ++++++--- tests/test_twopoint_difference.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 1c34b68b..e9151009 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -178,7 +178,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask -# print(jump_mask[0:300,:, 0, 0]) + trimmed_mask = jump_mask[:,1:-1,:,:] +# print(trimmed_mask[0:300,:, 0, 0]) + print(np.sum(trimmed_mask)) else: ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ sigma[np.newaxis, :, :] @@ -192,8 +194,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask = masked_ratio.mask jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False + fits.writeto("jump_mask.fits", jump_mask, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) - + print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) @@ -270,7 +273,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - + fits.writeto("output_2pt_gdq.fits", gdq, overwrite=True) return gdq, row_below_gdq, row_above_gdq diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index aebacca8..a9381182 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1027,8 +1027,8 @@ def test_sigma_clip(): hdul = fits.open('TSOjump_sc__linearity.fits') data = hdul['SCI'].data gdq = hdul['GROUPDQ'].data - indata = np.reshape(data[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) - ingdq = np.reshape(gdq[:, :, 20:21, 40:41], (data.shape[0], data.shape[1], 1, 1)) + indata = np.reshape(data[:, :, 20:22, 40:42], (data.shape[0], data.shape[1], 2, 2)) + ingdq = np.reshape(gdq[:, :, 20:22, 40:42], (data.shape[0], data.shape[1], 2, 2)) read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() From 1c7c7d3ead96f8c23d5fc18c66c7a5a102269ac6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 16:46:24 -0500 Subject: [PATCH 043/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index e9151009..9003e21f 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -194,7 +194,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask = masked_ratio.mask jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False - fits.writeto("jump_mask.fits", jump_mask, overwrite=True) + fits.writeto("jump_mask.fits", jump_mask *1.0, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel From 137d6967f3cc4c7104adcae83e723571f78ec449 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 18:00:59 -0500 Subject: [PATCH 044/196] fixed axis=1 to axis=0,1 --- src/stcal/jump/twopoint_difference.py | 11 +++++++---- tests/test_twopoint_difference.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 9003e21f..20a968ac 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -146,6 +146,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) + fits.writeto("first_diffs_mask.fits", first_diffs_masked.mask *1.0, overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -167,8 +168,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( str(total_groups), str(minimum_selfcal_groups))) + mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0,1)) + fits.writeto("stddev.fits", stddev, overwrite=True) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, - axis=1, masked=True) + axis=(0,1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) delta_diff = max_diffs - min_diffs @@ -178,9 +181,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask - trimmed_mask = jump_mask[:,1:-1,:,:] + trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) - print(np.sum(trimmed_mask)) + print("total masked pixels", np.sum(trimmed_mask)) else: ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ sigma[np.newaxis, :, :] @@ -194,7 +197,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask = masked_ratio.mask jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False - fits.writeto("jump_mask.fits", jump_mask *1.0, overwrite=True) +# fits.writeto("jump_mask.fits", jump_mask *1.0, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index a9381182..4236d361 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1024,11 +1024,11 @@ def test_median_func(): assert calc_med_first_diffs(arr)[0, 0] == -1 def test_sigma_clip(): - hdul = fits.open('TSOjump_sc__linearity.fits') - data = hdul['SCI'].data + hdul = fits.open('TSOjump_sc__refpix.fits') + data = hdul['SCI'].data * 4.0 gdq = hdul['GROUPDQ'].data - indata = np.reshape(data[:, :, 20:22, 40:42], (data.shape[0], data.shape[1], 2, 2)) - ingdq = np.reshape(gdq[:, :, 20:22, 40:42], (data.shape[0], data.shape[1], 2, 2)) + indata = np.reshape(data[:, :, 20:40, 40:60], (data.shape[0], data.shape[1], 20, 20)) + ingdq = np.reshape(gdq[:, :, 20:40, 40:60], (data.shape[0], data.shape[1], 20, 20)) read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() From 4f9dbc9bb3e2fbdef5ac73dc5c29bbb2c703f4bb Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 18:20:05 -0500 Subject: [PATCH 045/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 20a968ac..2749e27a 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -102,8 +102,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq = group_dq.copy() else: gdq = group_dq - fits.writeto("gdq_in.fits", gdq, overwrite=True) - # Get data characteristics + # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape ndiffs = ngroups - 1 @@ -146,7 +145,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - fits.writeto("first_diffs_mask.fits", first_diffs_masked.mask *1.0, overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -169,7 +167,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( str(total_groups), str(minimum_selfcal_groups))) mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0,1)) - fits.writeto("stddev.fits", stddev, overwrite=True) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, axis=(0,1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) @@ -276,7 +273,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - fits.writeto("output_2pt_gdq.fits", gdq, overwrite=True) return gdq, row_below_gdq, row_above_gdq From 4c8091ffafaf86eeee726f41ef704ac125cf7899 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 6 Feb 2023 18:26:10 -0500 Subject: [PATCH 046/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 2749e27a..13c949e3 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -251,7 +251,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, group, row, col + 1] & dnu_flag) == 0: gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) - + print("finish flag 4 neighbors") # flag n groups after jumps above the specified thresholds to account for # the transient seen after ramp jumps for integ in range(nints): @@ -273,6 +273,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + print("finish flag after jump") return gdq, row_below_gdq, row_above_gdq From 13f1e205938cc4874db4aa78119fe533513ef671 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 7 Feb 2023 11:59:41 -0500 Subject: [PATCH 047/196] Update twopoint_difference.py print statements --- src/stcal/jump/twopoint_difference.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 13c949e3..b5e05735 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -180,7 +180,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask = clipped_diffs.mask trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) - print("total masked pixels", np.sum(trimmed_mask)) + print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* + trimmed_mask.shape[3]) + print("done") else: ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ sigma[np.newaxis, :, :] From 95d146d11b11f07cb6dc7d966f8d15078ac830f7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 7 Feb 2023 16:44:53 -0500 Subject: [PATCH 048/196] fix flagging fix flagging --- src/stcal/jump/twopoint_difference.py | 1 + tests/test_ramp_fitting.py | 32 +++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index b5e05735..c735a89a 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -196,6 +196,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask = masked_ratio.mask jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False # fits.writeto("jump_mask.fits", jump_mask *1.0, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) print("start flag 4 neighbors") diff --git a/tests/test_ramp_fitting.py b/tests/test_ramp_fitting.py index 0fa58f59..2fc42cac 100644 --- a/tests/test_ramp_fitting.py +++ b/tests/test_ramp_fitting.py @@ -1,5 +1,5 @@ import numpy as np - +from astropy.io import fits from stcal.ramp_fitting.ramp_fit import ramp_fit_data from stcal.ramp_fitting.ramp_fit_class import RampData @@ -1046,7 +1046,35 @@ def test_new_saturation(): [[0.03073696, 0. , 0.]]]) np.testing.assert_allclose(cerr, check, tol, tol) - +def test_contiguous_jumps(): +# hdul = fits.open('/users/mregan/downloads/dark_imager_wthfaint_1_jump.fits') + hdul = fits.open('TSOjump_sc__jump.fits') +# hdul = fits.open('/users/mregan/downloads/dark_imager_baseline_2_00_jump.fits') + jumpdiff = np.diff(hdul['SCI'].data, axis=1) + data = hdul['SCI'].data + gdq = hdul['groupdq'].data + ypix = 342 + xpix = 37 + indata = data[:, :, ypix:ypix+2, xpix:xpix+2] + indataraw = data[0, :, ypix:ypix+2, xpix:xpix+2] + ingdq = gdq[:, :, ypix:ypix+2, xpix:xpix+2] + stx1 = 1 + +# ingdq[135] = 125 + nints, ngroups, nrows, ncols = indata.shape[0], indata.shape[1], 2, 2 + rnoise_val, gain_val = 100000, 1 + nframes, gtime, dtime = 1, 2.77, 2.77 + dims = (nints, ngroups, nrows, ncols) + var = (rnoise_val, gain_val) + tm = (nframes, gtime, dtime) + ramp_data, rnoise, gain = setup_inputs(dims, var, tm) + ramp_data.data[:, :, :, :] = indata + ramp_data.groupdq[:, :, :, :] = ingdq + buffsize, save_opt, algo, wt, ncores = 512, True, "OLS", "optimal", "none" + slopes, cube, optional, gls_dummy = ramp_fit_data( + ramp_data, buffsize, save_opt, rnoise, gain, algo, wt, ncores, dqflags) + fits.writeto("slopes.fits", slopes[0], overwrite=True) + print(slopes) def create_blank_ramp_data(dims, var, tm): """ Create empty RampData classes, as well as gain and read noise arrays, From 279deda439bd9c606ce4bad8bd180bb17812c530 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 7 Feb 2023 17:31:22 -0500 Subject: [PATCH 049/196] Update jump.py --- src/stcal/jump/jump.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 5cdd9315..bc2cec0f 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -219,13 +219,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2) - # This is the flag that controls the flagging of either snowballs or showers. - if expand_large_events: - flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, - min_jump_area=min_jump_area, - expand_factor=expand_factor, use_ellipses=use_ellipses, - sat_required_snowball=sat_required_snowball) - else: yinc = int(n_rows / n_slices) slices = [] @@ -300,12 +293,12 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, k += 1 # This is the flag that controls the flagging of either snowballs or showers. - if expand_large_events: - flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, - min_jump_area=min_jump_area, - expand_factor=expand_factor, use_ellipses=use_ellipses, - sat_required_snowball=sat_required_snowball, - edge_size=edge_size) + if expand_large_events: + flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, + min_jump_area=min_jump_area, + expand_factor=expand_factor, use_ellipses=use_ellipses, + sat_required_snowball=sat_required_snowball, + edge_size=edge_size) elapsed = time.time() - start log.info('Total elapsed time = %g sec' % elapsed) From 852951cfe7cb0dcca86f0106e16773372396dee5 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 7 Feb 2023 18:28:25 -0500 Subject: [PATCH 050/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index c735a89a..cb9d752b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -138,10 +138,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # set 'saturated' or 'do not use' pixels to nan in data dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan dat[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan + dat[np.where(np.bitwise_and(gdq, dnu_flag + sat_flag))] = np.nan # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) + fits.writeto("first_diffs.fits", first_diffs, overwrite=True) # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) From f2cb8e563fcae82be36396c3f1c3afdf247764b0 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 7 Feb 2023 21:01:00 -0500 Subject: [PATCH 051/196] type problem --- src/stcal/jump/twopoint_difference.py | 14 ++++++++++---- tests/test_twopoint_difference.py | 12 +++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index cb9d752b..1219dfc6 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -143,11 +143,13 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) - fits.writeto("first_diffs.fits", first_diffs, overwrite=True) +# fits.writeto("first_diffs.fits", first_diffs, overwrite=True) # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) + fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) +# fits.writeto("median_diffs.fits", median_diffs.filled(fill_value=np.nan), overwrite=True) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -170,7 +172,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, str(total_groups), str(minimum_selfcal_groups))) mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0,1)) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, - axis=(0,1), masked=True) + axis=(0, 1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) delta_diff = max_diffs - min_diffs @@ -180,6 +182,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask +# fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* @@ -199,8 +202,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False -# fits.writeto("jump_mask.fits", jump_mask *1.0, overwrite=True) - gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * dqflags["JUMP_DET"]) + fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) + fits.writeto("incoming_gdq.fits",gdq, overwrite=True) + gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * + np.uint8(dqflags["JUMP_DET"])) + print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 4236d361..4a6b3822 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1027,17 +1027,19 @@ def test_sigma_clip(): hdul = fits.open('TSOjump_sc__refpix.fits') data = hdul['SCI'].data * 4.0 gdq = hdul['GROUPDQ'].data - indata = np.reshape(data[:, :, 20:40, 40:60], (data.shape[0], data.shape[1], 20, 20)) - ingdq = np.reshape(gdq[:, :, 20:40, 40:60], (data.shape[0], data.shape[1], 20, 20)) + indata = data[:14, :, :, :] + ingdq = gdq[:14, :, :, :] read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() - find_crs(indata, ingdq, read_noise, 3, + gdq, row_below_gdq, row_above_gdq = find_crs(indata, ingdq, read_noise, 3, 4, 5, 1, - True, 1000, + False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=100,) \ No newline at end of file + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50,) + fits.writeto("outgdq.fits", gdq, overwrite=True) + print('done') \ No newline at end of file From 1f9670814b78555c28b52e235361abc2b98c6584 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 8 Feb 2023 08:00:57 -0500 Subject: [PATCH 052/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 1219dfc6..b65c16b3 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -143,7 +143,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) -# fits.writeto("first_diffs.fits", first_diffs, overwrite=True) + fits.writeto("first_diffs.fits", first_diffs, overwrite=True) # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) @@ -182,7 +182,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask -# fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) + fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* From 41ef1e2b3a94dd1129383a2a31268a2d46718150 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 8 Feb 2023 08:24:46 -0500 Subject: [PATCH 053/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index b65c16b3..7b8735b2 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -118,7 +118,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, dnu_flag = dqflags["DO_NOT_USE"] jump_flag = dqflags["JUMP_DET"] - # get data, gdq for this integration + # get data, gdq dat = dataa num_flagged_grps = 0 # determine the number of groups with all pixels set to DO_NOT_USE @@ -206,7 +206,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, fits.writeto("incoming_gdq.fits",gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - + #if grp is all jump set to do not use + for integ in range(dat.shape[0]): + for grp in range(dat.shape[1]): + if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), + np.bitwise_and(gdq[integ, grp, :, :], sat_flag))): + gdq[integ, grp, :, :] = dnu_flag print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) From bd753eae469d465e8388e664b2660468537ba288 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 8 Feb 2023 08:34:12 -0500 Subject: [PATCH 054/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 7b8735b2..3fbfedd4 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -143,11 +143,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) - fits.writeto("first_diffs.fits", first_diffs, overwrite=True) +# fits.writeto("first_diffs.fits", first_diffs, overwrite=True) # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) +# fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # fits.writeto("median_diffs.fits", median_diffs.filled(fill_value=np.nan), overwrite=True) # calculate sigma for each pixel @@ -182,7 +182,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask - fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) +# fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* @@ -202,8 +202,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False - fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) - fits.writeto("incoming_gdq.fits",gdq, overwrite=True) +# fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) +# fits.writeto("incoming_gdq.fits",gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) #if grp is all jump set to do not use From 3cdb077875c17a3f8111c8e4503d89c8f69aaa82 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 8 Feb 2023 08:55:18 -0500 Subject: [PATCH 055/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 3fbfedd4..65a08cc0 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -211,7 +211,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for grp in range(dat.shape[1]): if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), np.bitwise_and(gdq[integ, grp, :, :], sat_flag))): - gdq[integ, grp, :, :] = dnu_flag + gdq[integ, grp, :, :] = 0 print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) From 7f41e2a0736c34ae75962521c684f5e867c03268 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 8 Feb 2023 16:42:23 -0500 Subject: [PATCH 056/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 65a08cc0..7e332c24 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -162,7 +162,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, e_jump = first_diffs - median_diffs[np.newaxis, :, :] # if nints > 1: -# else: if total_groups >= minimum_selfcal_groups: From 664de3532a53132bd9c5b64478ed30392c3f05ec Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 16:55:29 -0400 Subject: [PATCH 057/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 7e332c24..25b56c1d 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -169,7 +169,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( str(total_groups), str(minimum_selfcal_groups))) - mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0,1)) + mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0, 1)) + fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, axis=(0, 1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) From e9af6c06a3e39189f3486c612f4d417599f60e20 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 17:33:11 -0400 Subject: [PATCH 058/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 25b56c1d..51ff8ae8 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -149,7 +149,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) # fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) -# fits.writeto("median_diffs.fits", median_diffs.filled(fill_value=np.nan), overwrite=True) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) From b8698faf08fd026bb684e7a06cbc4639090bd680 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 18:56:52 -0400 Subject: [PATCH 059/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 51ff8ae8..f8622e24 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -176,6 +176,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) delta_diff = max_diffs - min_diffs rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) + fits.writeto("rms_diff.fits", rms_diff, overwrite=True) avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) out_avg = avg_diff.filled(fill_value=np.nan) out_rms = rms_diff.filled(fill_value=np.nan) From 935dd25a6ead143ad5a8a8793591704f1805d982 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 19:08:52 -0400 Subject: [PATCH 060/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index f8622e24..c3581d51 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -176,10 +176,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) delta_diff = max_diffs - min_diffs rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) - fits.writeto("rms_diff.fits", rms_diff, overwrite=True) avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) out_avg = avg_diff.filled(fill_value=np.nan) out_rms = rms_diff.filled(fill_value=np.nan) + fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask # fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) From fdd8179ad20245f2b384d6dad2e401222fe160c3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 19:38:05 -0400 Subject: [PATCH 061/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index c3581d51..2465706b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -177,7 +177,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, delta_diff = max_diffs - min_diffs rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) - out_avg = avg_diff.filled(fill_value=np.nan) + fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) out_rms = rms_diff.filled(fill_value=np.nan) fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) From fb8525f7d014dc7d088e6bd3ed8fd49592158ed6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 20:00:40 -0400 Subject: [PATCH 062/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 2465706b..acdb9756 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -166,8 +166,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if total_groups >= minimum_selfcal_groups: ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] - log.info(" Jump Step using selfcal sigma clip {} greater than {}".format( - str(total_groups), str(minimum_selfcal_groups))) + log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( + str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0, 1)) fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, From d26b062857dabfd9f3b99a1ebd2f6683b2c632e0 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 27 Mar 2023 20:20:34 -0400 Subject: [PATCH 063/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index acdb9756..bc7beeac 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -161,14 +161,13 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, e_jump = first_diffs - median_diffs[np.newaxis, :, :] # if nints > 1: - - if total_groups >= minimum_selfcal_groups: ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) - mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), axis=(0, 1)) + mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), sigma=normal_rej_thresh, + axis=(0, 1)) fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, axis=(0, 1), masked=True) From ab435de6178936821f43758376d328eeacc26fa1 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 28 Mar 2023 10:19:49 -0400 Subject: [PATCH 064/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index bc7beeac..587eb881 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -166,10 +166,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) - mean, median, stddev = stats.sigma_clipped_stats(np.abs(first_diffs_masked), sigma=normal_rej_thresh, + mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1)) fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) - clipped_diffs = stats.sigma_clip(np.abs(first_diffs_masked), sigma=normal_rej_thresh, + clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) From 63b1dca35c06bbbc0a48f3617acb9ed5a15cc7ba Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 28 Mar 2023 14:48:15 -0400 Subject: [PATCH 065/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 587eb881..44876b93 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -5,6 +5,7 @@ log.setLevel(logging.DEBUG) from astropy.io import fits + def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, two_diff_rej_thresh, three_diff_rej_thresh, nframes, flag_4_neighbors, max_jump_to_flag_neighbors, From 64cdf599faddd3b6c42523609b1f1847d72c76b9 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 13 Apr 2023 13:38:08 -0400 Subject: [PATCH 066/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 44876b93..b233269e 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -148,8 +148,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) -# fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) + fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) +# fits.writeto("median_diffs.fits", median_diffs, overwrite=True) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -182,7 +183,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask -# fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) + fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-1, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* From 9434171d5ad446eaca9a855b35e9f894fd5224f3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 13 Apr 2023 13:52:36 -0400 Subject: [PATCH 067/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index b233269e..2f9ca69b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -203,16 +203,17 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False -# fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) -# fits.writeto("incoming_gdq.fits",gdq, overwrite=True) + fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) + fits.writeto("incoming_gdq.fits",gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) + fits.writeto("new_gdq.fis", gdq, overwrite=True) #if grp is all jump set to do not use for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), np.bitwise_and(gdq[integ, grp, :, :], sat_flag))): - gdq[integ, grp, :, :] = 0 + gdq[integ, grp, :, :] = dnu_flag print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) From 8567dce7366a3bd20bf362f9fa7960db20bcb590 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 13 Apr 2023 15:56:07 -0400 Subject: [PATCH 068/196] test sigma clip --- src/stcal/jump/twopoint_difference.py | 12 +++++++----- tests/test_twopoint_difference.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 2f9ca69b..8944699d 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -164,8 +164,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # if nints > 1: if total_groups >= minimum_selfcal_groups: - ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ - sigma[np.newaxis, np.newaxis, :, :] +# ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ +# sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, @@ -184,7 +184,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_diffs = delta_diff.filled(fill_value=np.nan) jump_mask = clipped_diffs.mask fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) - trimmed_mask = jump_mask[:, 4:-1, :, :] + trimmed_mask = jump_mask[:, 4:-4, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* trimmed_mask.shape[3]) @@ -212,8 +212,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), - np.bitwise_and(gdq[integ, grp, :, :], sat_flag))): - gdq[integ, grp, :, :] = dnu_flag + np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): + jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) + gdq[integ, grp, jumpy, jumpx] = 0 + fits.writeto("new_gdq2.fits", gdq, overwrite=True) print("start flag 4 neighbors") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 4a6b3822..b0e8b2f0 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1024,7 +1024,8 @@ def test_median_func(): assert calc_med_first_diffs(arr)[0, 0] == -1 def test_sigma_clip(): - hdul = fits.open('TSOjump_sc__refpix.fits') +# hdul = fits.open('TSOjump_sc__refpix.fits') + hdul = fits.open('lrs_TSOjump_sigmaclip5_00_refpix.fits') data = hdul['SCI'].data * 4.0 gdq = hdul['GROUPDQ'].data indata = data[:14, :, :, :] From 2309e3e925454c4f8277030174fd7b9866bc51d3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 13 Apr 2023 16:11:05 -0400 Subject: [PATCH 069/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 8944699d..70dd8635 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -216,7 +216,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 fits.writeto("new_gdq2.fits", gdq, overwrite=True) - print("start flag 4 neighbors") + print("start flag 4 neighbors part") if flag_4_neighbors: # iterate over each 'jump' pixel cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) From c0df3af22b549bfa28db68abdd498c1aa71e8f2f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 14 Apr 2023 10:07:38 -0400 Subject: [PATCH 070/196] Update jump.py --- src/stcal/jump/jump.py | 228 +++++++++++------------------------------ 1 file changed, 60 insertions(+), 168 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index db2463e5..020b5ce8 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -1,7 +1,6 @@ import logging -import warnings -from astropy.io import fits - +import multiprocessing +import time import numpy as np import cv2 as cv @@ -277,7 +276,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, ellipse_expand=extend_ellipse_expand_ratio, num_grps_masked=grps_masked_after_shower, max_extended_radius=max_extended_radius) - else: yinc = int(n_rows / n_slices) slices = [] @@ -441,83 +439,32 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, """ - log.info('TEST Flagging large events (snowballs, showers).') + log.info('Flagging large Snowballs') n_showers_grp = [] - n_showers_grp_ellipse = [] - fits.writeto("input_jump_cube.fits", gdq, overwrite=True) for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): - if (group//10) * 10 == group: - print("Grp", group) - else: - print("Grp", group, end=" ") - if use_ellipses: - new_flagged_pixels = 1.0*gdq[integration, group, :, :] - 1.0*gdq[integration, group - 1, :, :] - new_flagged_pixels[new_flagged_pixels < 0] = 0 - fits.writeto('new_flagged_pixels.fits', new_flagged_pixels, overwrite=True) - jump_ellipses = find_ellipses(new_flagged_pixels.astype('uint8'), jump_flag, min_jump_area) - n_showers_grp_ellipse.append(len(jump_ellipses)) - gdq[integration, group, :, :], num_events = \ - extend_ellipses(gdq[integration, group, :, :], - jump_ellipses, sat_flag, jump_flag, - expansion=expand_factor) - else: - current_gdq = 1.0 * gdq[integration, group, :, :] - prev_gdq = 1.0 * gdq[integration, group - 1, :, :] - diff_gdq = 1.0 * current_gdq - prev_gdq - diff_gdq[diff_gdq != sat_flag] = 0 - new_sat = diff_gdq.astype('uint8') -# fits.writeto("diff_gdq.fits", diff_gdq, overwrite=True) -# fits.writeto('current_gdq.fits', current_gdq, overwrite = True) -# fits.writeto('prev_gdq.fits', prev_gdq, overwrite=True) -# new_flagged_pixels = gdq[integration, group, :, :] - gdq[integration, group - 1, :, :] - fits.writeto("new_sat.fits", new_sat, overwrite=True) - # find the ellipse parameters for newly saturated pixels - sat_ellipses = find_ellipses(new_sat, sat_flag, min_sat_area) - # expand the larger saturated cores to deal with the charge migration from the - # saturated cores. -# gdq[integration, , :, :] = extend_saturation(gdq[integration, :, :, :], - gdq[integration, :, :, :] = extend_saturation(gdq[integration, :, :, :], - group, sat_ellipses, sat_flag, jump_flag, - min_sat_radius_extend, expansion=sat_expand) -# fits.writeto("after_extend_large_events.fits", gdq, overwrite=True) - # recalculate the newly flagged pixels after the expansion of saturation - current_gdq = 1.0 * gdq[integration, group, :, :] - prev_gdq = 1.0 * gdq[integration, group - 1, :, :] - diff_gdq = 1.0 * current_gdq - prev_gdq - diff_gdq[diff_gdq < 0] = 0 - new_sat = diff_gdq.astype('uint8') -# fits.writeto("diff_gdq2.fits", diff_gdq, overwrite=True) -# fits.writeto('current_gdq2.fits', current_gdq, overwrite=True) -# fits.writeto('prev_gdq2.fits', prev_gdq, overwrite=True) - # find all the newly saturated pixel - sat_pixels = np.bitwise_and(diff_gdq.astype('uint8'), sat_flag) - saty, satx = np.where(sat_pixels == sat_flag) - only_jump = diff_gdq.copy() - fits.writeto("onlyjump.fits", only_jump, overwrite=True) - # reset the saturated pixel to be jump to allow the jump circles to have the - # central saturated region set to "jump" instead of "saturation". - only_jump[saty, satx] = jump_flag - fits.writeto("onlyjump2.fits", only_jump, overwrite=True) - # only_jump_cube[integration, group, :, :] = only_jump - jump_ellipses = find_ellipses(only_jump.astype('uint8'), jump_flag, min_jump_area) - if sat_required_snowball: - low_threshold = edge_size - high_threshold = gdq.shape[2] - edge_size - snowballs = make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold) - else: - snowballs = jump_ellipses - n_showers_grp.append(len(snowballs)) - gdq[integration, group, :, :], num_events = extend_ellipses(gdq[integration, group, :, :], - snowballs, sat_flag, - jump_flag, - expansion=expand_factor) -# fits.writeto("final_gdq.fits", gdq[integration, group,:, :], overwrite=True) - fits.writeto("last_gdq_inside.fits", gdq, overwrite=True) - if use_ellipses: - if np.all(np.array(n_showers_grp_ellipse) == 0): - log.info(f'No showers found in integration {integration}.') + current_gdq = 1.0 * gdq[integration, group, :, :] + prev_gdq = 1.0 * gdq[integration, group - 1, :, :] + diff_gdq = 1.0 * current_gdq - prev_gdq + diff_gdq[diff_gdq != sat_flag] = 0 + new_sat = diff_gdq.astype('uint8') + # find the ellipse parameters for newly saturated pixels + sat_ellipses = find_ellipses(new_sat, sat_flag, min_sat_area) + + # find the ellipse parameters for jump regions + jump_ellipses = find_ellipses(gdq[integration, group, :, :], + jump_flag, min_jump_area) + if sat_required_snowball: + low_threshold = edge_size + high_threshold = max(0, gdq.shape[2] - edge_size) + + gdq, snowballs = make_snowballs(gdq, integration, group, + jump_ellipses, sat_ellipses, + low_threshold, high_threshold, + min_sat_radius_extend, + sat_expand, sat_flag, + max_extended_radius) else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) @@ -532,45 +479,20 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, log.info(f' In integration {integration}, number of snowballs ' + f'in each group = {n_showers_grp}') -def extend_snowballs(plane, snowballs, sat_flag, jump_flag, expansion=1.5): - # For a given DQ plane it will use the list of snowballs to create expanded circles of pixels with - # the jump flag set. - image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) - num_circles = len(snowballs) - sat_pix = np.bitwise_and(plane, sat_flag) - for snowball in snowballs: - jump_radius = snowball[1] - jump_center = snowball[0] - cenx = jump_center[1] - ceny = jump_center[0] - extend_radius = round(jump_radius * expansion) - image = cv.circle(image, (round(ceny), round(cenx)), extend_radius, (0, 0, 4), -1) - jump_circle = image[:, :, 2] - saty, satx = np.where(sat_pix == sat_flag) - jump_circle[saty, satx] = 0 - plane = np.bitwise_or(plane, jump_circle) - - return plane, num_circles - - -def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, - min_sat_radius_extend, expansion=2): + +def extend_saturation(cube, grp, sat_ellipses, sat_flag, + min_sat_radius_extend, expansion=2, + max_extended_radius=200): image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) - jump_pix = np.bitwise_and(cube[grp, :, :], jump_flag) outcube = cube.copy() for ellipse in sat_ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] minor_axis = min(ellipse[1][1], ellipse[1][0]) - -# print("Grp", grp, " radius ", minor_axis, "count", count, "center", ellipse[0]) - if minor_axis > min_sat_radius_extend: axis1 = ellipse[1][0] + expansion axis2 = ellipse[1][1] + expansion alpha = ellipse[2] - image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 + 0.5), - round(axis2 + 0.5)), alpha, 0, 360, (0, 0, 22), -1) axis1 = min(axis1, max_extended_radius) axis2 = min(axis2, max_extended_radius) image = cv.ellipse(image, (round(ceny), round(cenx)), @@ -582,18 +504,15 @@ def extend_saturation(cube, grp, sat_ellipses, sat_flag, jump_flag, return outcube - - def extend_ellipses(gdq_cube, intg, grp, ellipses, sat_flag, jump_flag, expansion=1.9, expand_by_ratio=True, num_grps_masked=1, max_extended_radius=200): # For a given DQ plane it will use the list of ellipses to create - # expanded ellipses of pixels with>>>>>>> 4e967f89e0d11a9de159aebe178ba44cae154e24 + # expanded ellipses of pixels with # the jump flag set. plane = gdq_cube[intg, grp, :, :] image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) num_ellipses = len(ellipses) - sat_pix = np.bitwise_and(plane, sat_flag) for ellipse in ellipses: ceny = ellipse[0][0] cenx = ellipse[0][1] @@ -616,17 +535,21 @@ def extend_ellipses(gdq_cube, intg, grp, ellipses, sat_flag, jump_flag, axis2 = ellipse[1][1] + expansion axis1 = min(axis1, max_extended_radius) axis2 = min(axis2, max_extended_radius) - alpha = ellipse[2] image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), round(axis2 / 2)), alpha, 0, 360, (0, 0, jump_flag), -1) jump_ellipse = image[:, :, 2] - # don't add any jump flags to pixels that are already saturated - saty, satx = np.where(sat_pix == sat_flag) - jump_ellipse[saty, satx] = 0 - plane = np.bitwise_or(plane, jump_ellipse) - return plane, num_ellipses + last_grp = min(grp + num_grps_masked, gdq_cube.shape[1]) + # This loop will flag the number of groups + for flg_grp in range(grp, last_grp): + sat_pix = np.bitwise_and(gdq_cube[intg, flg_grp, :, :], sat_flag) + saty, satx = np.where(sat_pix == sat_flag) + jump_ellipse[saty, satx] = 0 + gdq_cube[intg, flg_grp, :, :] = \ + np.bitwise_or(gdq_cube[intg, flg_grp, :, :], jump_ellipse) + return gdq_cube, num_ellipses + def find_circles(dqplane, bitmask, min_area): # Using an input DQ plane this routine will find the groups of pixels with at least the minimum @@ -653,75 +576,44 @@ def find_ellipses(dqplane, bitmask, min_area): return ellipses -def make_snowballs(jump_ellipses, sat_ellipses, low_threshold, high_threshold): - # Ths routine will create a list of snowballs (ellipses) that have the center of the saturation circle - # within the enclosing jump rectangle. +def make_snowballs(gdq, integration, group, jump_ellipses, sat_ellipses, + low_threshold, high_threshold, + min_sat_radius, expansion, sat_flag, max_extended_radius): + # Ths routine will create a list of snowballs (ellipses) that have the + # center + # of the saturation circle within the enclosing jump rectangle. snowballs = [] for jump in jump_ellipses: - sat_found = False if near_edge(jump, low_threshold, high_threshold): snowballs.append(jump) else: for sat in sat_ellipses: # center of saturation is within the enclosing jump rectangle if point_inside_ellipse(sat[0], jump): - if jump not in snowballs: - snowballs.append(jump) -# print("sat inside found", sat, jump) - sat_found = True -# if not sat_found: -# print("no saturation within jump rectangle ", jump) - return snowballs - - -def old_point_inside_ellipse(point, ellipse): - box = cv.boxPoints(ellipse) - ceny = ellipse[0][0] - cenx = ellipse[0][1] - axis1 = ellipse[1][0] - axis2 = ellipse[1][1] - theta = np.deg2rad(ellipse[2]) - pointx = point[0] - pointy = point[1] - radius = ((np.cos(theta) * (pointx - cenx) + np.sin(theta) * (pointy - ceny))**2)/axis2**2 + \ - ((np.sin(theta) * (pointx - cenx) + np.cos(theta) * (pointy - ceny))**2)/axis1**2 - if radius < 1: - return True - else: - return False + # center of jump should be saturated + jump_center = jump[0] + if gdq[integration, group, round(jump_center[1]), + round(jump_center[0])] == sat_flag: + if jump not in snowballs: + snowballs.append(jump) + gdq[integration, :, :, :] = \ + extend_saturation(gdq[integration, :, :, :], + group, [sat], sat_flag, + min_sat_radius, + expansion=expansion, + max_extended_radius=max_extended_radius) + return gdq, snowballs def point_inside_ellipse(point, ellipse): - delta_center = np.sqrt((point[0]-ellipse[0][0])**2 + (point[1]-ellipse[0][1])**2) + delta_center = np.sqrt((point[0]-ellipse[0][0])**2 + + (point[1]-ellipse[0][1])**2) minor_axis = min(ellipse[1][0], ellipse[1][1]) if delta_center < minor_axis: return True else: return False -def point_inside_rectangle(point, ellipse): - box = cv.boxPoints(ellipse) - area1 = triangle_area(point, box[0], box[1]) - area2 = triangle_area(point, box[1], box[2]) - area3 = triangle_area(point, box[2], box[3]) - area4 = triangle_area(point, box[3], box[0]) - rectangle_area = ellipse[1][0] * ellipse[1][1] - triangle_area_sum = area1 + area2 + area3 + area4 - if triangle_area_sum > rectangle_area: - return False - else: - return True - -#Area = abs( (Bx * Ay - Ax * By) + -# (Cx * By - Bx * Cy) + -# (Ax * Cy - Cx * Ay) ) / 2 -def triangle_area(point, vert1, vert2): - area = np.abs((vert1[1] * point[0] - point[1] * vert1[0]) + - (vert2[1] * vert1[0] - vert1[1] * vert2[0]) + - (point[1] * vert2[0] - vert2[1] * point[0])) / 2 - return area - - def near_edge(jump, low_threshold, high_threshold): # This routing tests whether the center of a jump is close to the edge of From a7676faaec8fa5ac105a9adc985ebf605142ba1f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 14 Apr 2023 10:12:54 -0400 Subject: [PATCH 071/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 70dd8635..5874dfd2 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -164,8 +164,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # if nints > 1: if total_groups >= minimum_selfcal_groups: -# ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ -# sigma[np.newaxis, np.newaxis, :, :] + ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ + sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, From 6b3717aa9b3d6abdb9216482a8c48fc7157fb1f8 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 14 Apr 2023 11:28:58 -0400 Subject: [PATCH 072/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 5874dfd2..3fa95aac 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -182,8 +182,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, out_rms = rms_diff.filled(fill_value=np.nan) fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) - jump_mask = clipped_diffs.mask - fits.writeto("jump_mask.fits", jump_mask * 1.0, overwrite=True) + # jump_mask = 1.0 * clipped_diffs.mask - 1.0 * first_diffs_masked.mask + jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) + fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) + fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) + fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-4, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* @@ -204,10 +207,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) - fits.writeto("incoming_gdq.fits",gdq, overwrite=True) + fits.writeto("incoming_gdq.fits", gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - fits.writeto("new_gdq.fis", gdq, overwrite=True) + fits.writeto("new_gdq.fits", gdq, overwrite=True) #if grp is all jump set to do not use for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): From 36355a2df49f6a456012517d1b57e71214025c37 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 24 Apr 2023 15:30:17 -0400 Subject: [PATCH 073/196] add total CR count --- src/stcal/jump/jump.py | 9 +++++---- src/stcal/jump/twopoint_difference.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 020b5ce8..9c2aa951 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -246,7 +246,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, n_slices = max_available if n_slices == 1: - gdq, row_below_dq, row_above_dq = \ + gdq, row_below_dq, row_above_dq, total_primary_crs = \ twopt.find_crs(data, gdq, readnoise_2d, rejection_thresh, three_grp_thresh, four_grp_thresh, frames_per_group, flag_4_neighbors, max_jump_to_flag_neighbors, @@ -255,7 +255,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2) - + print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either snowballs. if expand_large_events: flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, @@ -330,11 +330,12 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # Reconstruct gdq, the row_above_gdq, and the row_below_gdq from the # slice result for resultslice in real_result: - if len(real_result) == k + 1: # last result gdq[:, :, k * yinc:n_rows, :] = resultslice[0] + total_primary_crs = resultslice[3] else: gdq[:, :, k * yinc:(k + 1) * yinc, :] = resultslice[0] + total_primary_crs += resultslice[3] row_below_gdq[:, :, :] = resultslice[1] row_above_gdq[:, :, :] = resultslice[2] if k != 0: @@ -352,7 +353,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # save the neighbors to be flagged that will be in the next slice previous_row_above_gdq = row_above_gdq.copy() k += 1 - + print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either # snowballs or showers. if expand_large_events: diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 3fa95aac..8acf468a 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -220,9 +220,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, grp, jumpy, jumpx] = 0 fits.writeto("new_gdq2.fits", gdq, overwrite=True) print("start flag 4 neighbors part") + cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) + num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel - cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) - for j in range(len(cr_group)): ratio_this_pix = ratio[cr_integ[j], cr_group[j] - 1, cr_row[j], cr_col[j]] @@ -297,7 +297,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) print("finish flag after jump") - return gdq, row_below_gdq, row_above_gdq + return gdq, row_below_gdq, row_above_gdq, num_primary_crs def calc_med_first_diffs(first_diffs): From 8baf448b564c18e5f22ebaa62762020dce8d8ae3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 25 Apr 2023 11:40:45 -0400 Subject: [PATCH 074/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 8acf468a..6100d3f3 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -14,7 +14,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=100,): + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=1000,): """ Find CRs/Jumps in each integration within the input data array. The input From 34b6b968d21393a79a01aea0dcc392425041de87 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 25 Apr 2023 12:02:20 -0400 Subject: [PATCH 075/196] Update jump.py --- src/stcal/jump/jump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 9c2aa951..bbf6d0c4 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -329,6 +329,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # Reconstruct gdq, the row_above_gdq, and the row_below_gdq from the # slice result + total_primary_crs = 0 for resultslice in real_result: if len(real_result) == k + 1: # last result gdq[:, :, k * yinc:n_rows, :] = resultslice[0] From e6bcb79e43e2eae23b14c4de19abe4fc3de9572b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 25 Apr 2023 12:04:34 -0400 Subject: [PATCH 076/196] Update jump.py --- src/stcal/jump/jump.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index bbf6d0c4..97399e15 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -333,12 +333,11 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, for resultslice in real_result: if len(real_result) == k + 1: # last result gdq[:, :, k * yinc:n_rows, :] = resultslice[0] - total_primary_crs = resultslice[3] else: gdq[:, :, k * yinc:(k + 1) * yinc, :] = resultslice[0] - total_primary_crs += resultslice[3] row_below_gdq[:, :, :] = resultslice[1] row_above_gdq[:, :, :] = resultslice[2] + total_primary_crs += resultslice[3] if k != 0: # For all but the first slice, flag any CR neighbors in the top # row of the previous slice and flag any neighbors in the From bc4e6609f038c940c85ef34d9bf97685553bae6f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 25 Apr 2023 12:36:15 -0400 Subject: [PATCH 077/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 6100d3f3..e9d1adca 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -296,7 +296,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - print("finish flag after jump") + print("finish flag after jump, num crs =", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs From 80d1793bec4ad6b21d8b22b46faddde1ca0bb9f9 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 25 Apr 2023 12:46:08 -0400 Subject: [PATCH 078/196] Update jump.py --- src/stcal/jump/jump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 97399e15..eebb42bf 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -338,6 +338,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, row_below_gdq[:, :, :] = resultslice[1] row_above_gdq[:, :, :] = resultslice[2] total_primary_crs += resultslice[3] + print("in Jump ", total_primary_crs) if k != 0: # For all but the first slice, flag any CR neighbors in the top # row of the previous slice and flag any neighbors in the From 1746a1206150fbf70945372922d5bbbf9c769933 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 10:53:21 -0400 Subject: [PATCH 079/196] Update jump.py --- src/stcal/jump/jump.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index eebb42bf..ccd64878 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -1,7 +1,7 @@ import logging import multiprocessing import time - +from astropy.io import fits import numpy as np import cv2 as cv @@ -707,6 +707,9 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, ratio.shape[2]), dtype=np.uint8) exty, extx = np.where(masked_smoothed_ratio > snr_threshold) extended_emission[exty, extx] = 1 + if grp == 138 and intg == 0: + fits.writeto("masked_smoothed_ratio.fits", masked_smoothed_ratio, overwrite=True) + fits.writeto("extended_emission.fits", extended_emission, overwrite=True) # find the contours of the extended emission contours, hierarchy = cv.findContours(extended_emission, cv.RETR_EXTERNAL, From da869b1fdd4737e1b7e076e0eede3cf0da0598c6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 10:57:37 -0400 Subject: [PATCH 080/196] Update jump.py --- src/stcal/jump/jump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index ccd64878..c7594b29 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -671,6 +671,7 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, Total number of showers detected. """ + print("find showers on sigmaclip") read_noise_2 = readnoise_2d**2 data = indata.copy() data[gdq == sat_flag] = np.nan From a3ae84b94a01137a80e71c7cacfcadd3145ac004 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 11:20:40 -0400 Subject: [PATCH 081/196] Update jump.py --- src/stcal/jump/jump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index c7594b29..db60d5d7 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -708,7 +708,8 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, ratio.shape[2]), dtype=np.uint8) exty, extx = np.where(masked_smoothed_ratio > snr_threshold) extended_emission[exty, extx] = 1 - if grp == 138 and intg == 0: + if grp == 179 and intg == 0: + fits.writeto("masked_ratio.fits",masked_ratio, overwrite=True) fits.writeto("masked_smoothed_ratio.fits", masked_smoothed_ratio, overwrite=True) fits.writeto("extended_emission.fits", extended_emission, overwrite=True) # find the contours of the extended emission From 9a5534ff68e645fd46e374dffb855a3ae9e0cd04 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 13:14:34 -0400 Subject: [PATCH 082/196] Update jump.py --- src/stcal/jump/jump.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index db60d5d7..55874aa3 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -722,6 +722,40 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, # get the minimum enclosing rectangle which is the same as the # minimum enclosing ellipse ellipses = [cv.minAreaRect(con) for con in bigcontours] + if grp==179 and intg == 0: + expand_by_ratio = True + expansion = 1.0 + plane = gdq[intg, grp, :, :] + image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) + num_ellipses = len(ellipses) + for ellipse in ellipses: + ceny = ellipse[0][0] + cenx = ellipse[0][1] + # Expand the ellipse by the expansion factor. The number of pixels + # added to both axes is + # the number of pixels added to the minor axis. This prevents very + # large flagged ellipses + # with high axis ratio ellipses. The major and minor axis are not + # always the same index. + # Therefore, we have to test to find which is actually the minor axis. + if expand_by_ratio: + if ellipse[1][1] < ellipse[1][0]: + axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] + axis2 = ellipse[1][1] * expansion + else: + axis1 = ellipse[1][0] * expansion + axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + else: + axis1 = ellipse[1][0] + expansion + axis2 = ellipse[1][1] + expansion + axis1 = min(axis1, max_extended_radius) + axis2 = min(axis2, max_extended_radius) + alpha = ellipse[2] + image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), + round(axis2 / 2)), alpha, 0, 360, + (0, 0, jump_flag), -1) + jump_ellipse = image[:, :, 2] + fits.writeto("jump_ellipse.fits", jump_ellipse, overwrite=True) if len(ellipses) > 0: # add all the showers for this integration to the list all_ellipses.append([intg, grp, ellipses]) From 8e0af8d34495a37e6810f69c550a8db95791dfbf Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 13:19:58 -0400 Subject: [PATCH 083/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index e9d1adca..1ad4d82b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -148,7 +148,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) +# fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # fits.writeto("median_diffs.fits", median_diffs, overwrite=True) # calculate sigma for each pixel @@ -170,7 +170,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1)) - fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) + # fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) @@ -178,15 +178,15 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, delta_diff = max_diffs - min_diffs rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) - fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) +# fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) out_rms = rms_diff.filled(fill_value=np.nan) - fits.writeto("rms_diff.fits", out_rms, overwrite=True) +# fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) # jump_mask = 1.0 * clipped_diffs.mask - 1.0 * first_diffs_masked.mask jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) - fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) - fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) - fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) +# fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) +# fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) +# fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-4, :, :] # print(trimmed_mask[0:300,:, 0, 0]) print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* @@ -206,11 +206,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False - fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) - fits.writeto("incoming_gdq.fits", gdq, overwrite=True) +# fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) +# fits.writeto("incoming_gdq.fits", gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - fits.writeto("new_gdq.fits", gdq, overwrite=True) +# fits.writeto("new_gdq.fits", gdq, overwrite=True) #if grp is all jump set to do not use for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): @@ -218,7 +218,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 - fits.writeto("new_gdq2.fits", gdq, overwrite=True) +# fits.writeto("new_gdq2.fits", gdq, overwrite=True) print("start flag 4 neighbors part") cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) From 081997378186a970cfa7947bbf431607db8234a7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 14:26:18 -0400 Subject: [PATCH 084/196] Update jump.py --- src/stcal/jump/jump.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 55874aa3..a65b8b51 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -723,10 +723,16 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, # minimum enclosing ellipse ellipses = [cv.minAreaRect(con) for con in bigcontours] if grp==179 and intg == 0: + expand_by_ratio = True expansion = 1.0 plane = gdq[intg, grp, :, :] image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) + image2 = np.zeros_like(image) + cv.drawContours(image2, bigcontours, -1, (0, 255, 0), 3) + big_contour_image = image2[:, :, 2] + fits.writeto("big_contour.fits", big_contour_image, overwrite=True) + fits.writeto("image2.fits", image2, overwrite=True) num_ellipses = len(ellipses) for ellipse in ellipses: ceny = ellipse[0][0] From 397fb4ea754ecd3a5aa42c5e00bc14847c2b3b16 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 14:35:31 -0400 Subject: [PATCH 085/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index a65b8b51..2330d0fb 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -729,7 +729,7 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, plane = gdq[intg, grp, :, :] image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) image2 = np.zeros_like(image) - cv.drawContours(image2, bigcontours, -1, (0, 255, 0), 3) + cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), 3) big_contour_image = image2[:, :, 2] fits.writeto("big_contour.fits", big_contour_image, overwrite=True) fits.writeto("image2.fits", image2, overwrite=True) From 9618b306ad1b8cf7b4cea7eef469782aed1eed77 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 27 Apr 2023 14:59:43 -0400 Subject: [PATCH 086/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 2330d0fb..cc2a4ff0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -729,7 +729,7 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, plane = gdq[intg, grp, :, :] image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) image2 = np.zeros_like(image) - cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), 3) + cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), -1) big_contour_image = image2[:, :, 2] fits.writeto("big_contour.fits", big_contour_image, overwrite=True) fits.writeto("image2.fits", image2, overwrite=True) From 1801be0f398d49d27f419e081d9c35bdb4d3bd3b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 25 May 2023 16:51:02 -0400 Subject: [PATCH 087/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 1ad4d82b..6bcef78b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -284,7 +284,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") +# log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") for j in range(len(cr_group)): group = cr_group[j] From ad304eed811c93476b9655c9f4b42ab55dfdf99b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 26 May 2023 13:20:40 -0400 Subject: [PATCH 088/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 6bcef78b..841a4941 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -14,7 +14,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=1000,): + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50,): """ Find CRs/Jumps in each integration within the input data array. The input @@ -96,7 +96,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pixels above current row also to be flagged as a CR """ - print("min groups ", minimum_groups ) +# print("min groups ", minimum_groups ) # copy data and group DQ array if copy_arrs: dataa = dataa.copy() @@ -128,8 +128,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps - print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", - minimum_selfcal_groups) +# print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", +# minimum_selfcal_groups) if total_groups < minimum_groups: log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") return gdq, row_below_gdq, row_above_gdq @@ -189,9 +189,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-4, :, :] # print(trimmed_mask[0:300,:, 0, 0]) - print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* - trimmed_mask.shape[3]) - print("done") +# print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* +# trimmed_mask.shape[3]) +# print("done") else: ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ sigma[np.newaxis, :, :] @@ -219,7 +219,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 # fits.writeto("new_gdq2.fits", gdq, overwrite=True) - print("start flag 4 neighbors part") +# print("start flag 4 neighbors part") cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel @@ -274,7 +274,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, group, row, col + 1] & dnu_flag) == 0: gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) - print("finish flag 4 neighbors") +# print("finish flag 4 neighbors") # flag n groups after jumps above the specified thresholds to account for # the transient seen after ramp jumps for integ in range(nints): From a72829cc3921a9056092a2f194342f8497c93493 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 11:51:37 -0400 Subject: [PATCH 089/196] tests --- src/stcal/jump/twopoint_difference.py | 18 +++++++++--------- tests/test_twopoint_difference.py | 27 +++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 841a4941..9e0542d6 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -170,7 +170,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1)) - # fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) + fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1), masked=True) max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) @@ -178,15 +178,15 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, delta_diff = max_diffs - min_diffs rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) -# fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) + fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) out_rms = rms_diff.filled(fill_value=np.nan) -# fits.writeto("rms_diff.fits", out_rms, overwrite=True) + fits.writeto("rms_diff.fits", out_rms, overwrite=True) out_diffs = delta_diff.filled(fill_value=np.nan) # jump_mask = 1.0 * clipped_diffs.mask - 1.0 * first_diffs_masked.mask jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) -# fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) -# fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) -# fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) + fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) + fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) + fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) trimmed_mask = jump_mask[:, 4:-4, :, :] # print(trimmed_mask[0:300,:, 0, 0]) # print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* @@ -207,10 +207,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False # fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) -# fits.writeto("incoming_gdq.fits", gdq, overwrite=True) + fits.writeto("incoming_gdq.fits", gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) -# fits.writeto("new_gdq.fits", gdq, overwrite=True) + fits.writeto("new_gdq.fits", gdq, overwrite=True) #if grp is all jump set to do not use for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): @@ -218,7 +218,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 -# fits.writeto("new_gdq2.fits", gdq, overwrite=True) + fits.writeto("new_gdq2.fits", gdq, overwrite=True) # print("start flag 4 neighbors part") cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index d8515767..5765fa14 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1030,7 +1030,7 @@ def test_sigma_clip(): read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() - gdq, row_below_gdq, row_above_gdq = find_crs(indata, ingdq, read_noise, 3, + gdq, row_below_gdq, row_above_gdq, total_primary_crs = find_crs(indata, ingdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, @@ -1040,4 +1040,27 @@ def test_sigma_clip(): after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50,) fits.writeto("outgdq.fits", gdq, overwrite=True) - print('done') \ No newline at end of file + print('done') + +def test_first_grp_flag_issue(): + nints = 8 + nrows = 2 + ncols = 2 + ngroups = 10 + rej_threshold = 3 + nframes = 1 + readnoise = 2 + data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + data = np.random.normal(0, readnoise, size=(nints, ngroups, nrows, ncols)) + read_noise = np.full((nrows, ncols), readnoise, dtype=np.float32) + gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint32) + + gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] + gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] + gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + after_jump_flag_e1=0.0, after_jump_flag_n1=0, + after_jump_flag_e2=0.0, after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) + fits.writeto("outgdq.fits",gdq, overwrite=True) \ No newline at end of file From f01aac172040c5d7c46b6afcae20792b5b43a937 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 12:16:05 -0400 Subject: [PATCH 090/196] Update jump.py --- src/stcal/jump/jump.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index cc2a4ff0..dd023f1a 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -34,7 +34,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, edge_size=25, extend_snr_threshold=1.2, extend_min_area=90, extend_inner_radius=1, extend_outer_radius=2.6, extend_ellipse_expand_ratio=1.2, grps_masked_after_shower=5, - max_extended_radius=200): + max_extended_radius=200, minimum_groups=3, + minimum_selfcal_groups=50): """ This is the high-level controlling routine for the jump detection process. @@ -254,7 +255,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_e1=after_jump_flag_e1, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, - after_jump_flag_n2=after_jump_flag_n2) + after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, + minimum_groups=3, minimum_selfcal_groups=50) print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either snowballs. if expand_large_events: @@ -289,7 +291,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # modified unless copied beforehand gdq = gdq.copy() data = data.copy() - copy_arrs = False # we dont need to copy arrays again in find_crs + copy_arrs = False # we don't need to copy arrays again in find_crs for i in range(n_slices - 1): slices.insert(i, (data[:, :, i * yinc:(i + 1) * yinc, :], @@ -301,7 +303,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, min_jump_to_flag_neighbors, dqflags, after_jump_flag_e1, after_jump_flag_n1, after_jump_flag_e2, after_jump_flag_n2, - copy_arrs)) + copy_arrs, minimum_groups, minimum_selfcal_groups)) # last slice get the rest slices.insert(n_slices - 1, (data[:, :, (n_slices - 1) * @@ -317,7 +319,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, min_jump_to_flag_neighbors, dqflags, after_jump_flag_e1, after_jump_flag_n1, after_jump_flag_e2, after_jump_flag_n2, - copy_arrs)) + copy_arrs, minimum_groups, minimum_selfcal_groups)) log.info("Creating %d processes for jump detection " % n_slices) pool = multiprocessing.Pool(processes=n_slices) # Starts each slice in its own process. Starmap allows more than one @@ -338,7 +340,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, row_below_gdq[:, :, :] = resultslice[1] row_above_gdq[:, :, :] = resultslice[2] total_primary_crs += resultslice[3] - print("in Jump ", total_primary_crs) if k != 0: # For all but the first slice, flag any CR neighbors in the top # row of the previous slice and flag any neighbors in the @@ -354,7 +355,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # save the neighbors to be flagged that will be in the next slice previous_row_above_gdq = row_above_gdq.copy() k += 1 - print("total primary CRs", total_primary_crs) + print("total primary CRs, multiple threads", total_primary_crs) # This is the flag that controls the flagging of either # snowballs or showers. if expand_large_events: From efa23d8bb46fa9efcefe2d0748e7befc965b2188 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 12:20:44 -0400 Subject: [PATCH 091/196] lower sig clip thres --- src/stcal/jump/jump.py | 2 +- src/stcal/jump/twopoint_difference.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index dd023f1a..9e64d51f 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -256,7 +256,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, - minimum_groups=3, minimum_selfcal_groups=50) + minimum_groups=3, minimum_selfcal_groups=30) print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either snowballs. if expand_large_events: diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 9e0542d6..96f4400a 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -14,7 +14,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50,): + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=30,): """ Find CRs/Jumps in each integration within the input data array. The input From 7710acdcfad9b99473e29082fb35791ec4f7f78e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 12:32:25 -0400 Subject: [PATCH 092/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 96f4400a..0a9cc7ab 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -195,6 +195,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, else: ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ sigma[np.newaxis, :, :] + fits.writeto("baselineratio.fits", ratio, overwrite=True) if ndiffs >= 4: masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) if ndiffs == 3: From 5691c176b7d95b083d05946b6c353e19761b16e2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 12:50:57 -0400 Subject: [PATCH 093/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 150 +++++++++++++++++++++----- 1 file changed, 122 insertions(+), 28 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 0a9cc7ab..e7fd5997 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -192,35 +192,129 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* # trimmed_mask.shape[3]) # print("done") + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False + # fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) + fits.writeto("incoming_gdq.fits", gdq, overwrite=True) + gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * + np.uint8(dqflags["JUMP_DET"])) + fits.writeto("new_gdq.fits", gdq, overwrite=True) + # if grp is all jump set to do not use + for integ in range(dat.shape[0]): + for grp in range(dat.shape[1]): + if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), + np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): + jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) + gdq[integ, grp, jumpy, jumpx] = 0 + fits.writeto("new_gdq2.fits", gdq, overwrite=True) else: - ratio = np.abs(first_diffs - median_diffs[np.newaxis, :, :]) / \ - sigma[np.newaxis, :, :] - fits.writeto("baselineratio.fits", ratio, overwrite=True) - if ndiffs >= 4: - masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) - if ndiffs == 3: - masked_ratio = np.ma.masked_greater(ratio, three_diff_rej_thresh) - if ndiffs == 2: - ratio = (first_diffs - median_diffs[np.newaxis, :, :]) / sigma[np.newaxis, :, :] - masked_ratio = np.ma.masked_greater(ratio, two_diff_rej_thresh) - jump_mask = masked_ratio.mask - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False -# fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) - fits.writeto("incoming_gdq.fits", gdq, overwrite=True) - gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * - np.uint8(dqflags["JUMP_DET"])) - fits.writeto("new_gdq.fits", gdq, overwrite=True) - #if grp is all jump set to do not use - for integ in range(dat.shape[0]): - for grp in range(dat.shape[1]): - if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), - np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): - jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) - gdq[integ, grp, jumpy, jumpx] = 0 - fits.writeto("new_gdq2.fits", gdq, overwrite=True) -# print("start flag 4 neighbors part") + for integ in range(nints): + + # get data, gdq for this integration + dat = dataa[integ] + gdq_integ = gdq[integ] + + # set 'saturated' or 'do not use' pixels to nan in data + dat[np.where(np.bitwise_and(gdq_integ, sat_flag))] = np.nan + dat[np.where(np.bitwise_and(gdq_integ, dnu_flag))] = np.nan + + # calculate the differences between adjacent groups (first diffs) + # use mask on data, so the results will have sat/donotuse groups masked + first_diffs = np.diff(dat, axis=0) + + # calc. the median of first_diffs for each pixel along the group axis + median_diffs = calc_med_first_diffs(first_diffs) + + # calculate sigma for each pixel + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + + # reset sigma so pxels with 0 readnoise are not flagged as jumps + sigma[np.where(sigma == 0.)] = np.nan + + # compute 'ratio' for each group. this is the value that will be + # compared to 'threshold' to classify jumps. subtract the median of + # first_diffs from first_diffs, take the abs. value and divide by sigma. + e_jump = first_diffs - median_diffs[np.newaxis, :, :] + ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] + + # create a 2d array containing the value of the largest 'ratio' for each group + max_ratio = np.nanmax(ratio, axis=0) + + # now see if the largest ratio of all groups for each pixel exceeds the threshold. + # there are different threshold for 4+, 3, and 2 usable groups + num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) + row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, + max_ratio > normal_rej_thresh)) + row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, + max_ratio > three_diff_rej_thresh)) + row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, + max_ratio > two_diff_rej_thresh)) + + log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' + log.info(log_str.format(len(row4cr), 'five or more')) + + # get the rows, col pairs for all pixels with at least one CR + all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) + all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) + + # iterate over all groups of the pix w/ an inital CR to look for subsequent CRs + # flag and clip the first CR found. recompute median/sigma/ratio + # and repeat the above steps of comparing the max 'ratio' for each pixel + # to the threshold to determine if another CR can be flagged and clipped. + # repeat this process until no more CRs are found. + for j in range(len(all_crs_row)): + + # get arrays of abs(diffs), ratio, readnoise for this pixel + pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] + pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] + pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] + + # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR + pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) + + # set the largest ratio as a CR + pix_cr_mask[np.nanargmax(pix_ratio)] = 0 + new_CR_found = True + + # loop and check for more CRs, setting the mask as you go and + # clipping the group with the CR. stop when no more CRs are found + # or there is only one two diffs left (which means there is + # actually one left, since the next CR will be masked after + # checking that condition) + + while new_CR_found and ((ndiffs - np.sum(np.isnan(pix_first_diffs))) > 2): + + new_CR_found = False + + # set CRs to nans in first diffs to clip them + pix_first_diffs[~pix_cr_mask] = np.nan + + # recalculate median, sigma, and ratio + new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) + + new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) + new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma + + # check if largest ratio exceeds threhold appropriate for num remaining groups + + # select appropriate thresh. based on number of remaining groups + rej_thresh = normal_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: + rej_thresh = three_diff_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: + rej_thresh = two_diff_rej_thresh + new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: + new_CR_found = True + pix_cr_mask[new_pix_max_ratio_idx] = 0 + + # Found all CRs for this pix - set flags in input DQ array + gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ + np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], + dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) + + # print("start flag 4 neighbors part") cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel From 20824980e821a1e263f28fab55a1b3840b1bc5f8 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 12:51:28 -0400 Subject: [PATCH 094/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 9e64d51f..dd023f1a 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -256,7 +256,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, - minimum_groups=3, minimum_selfcal_groups=30) + minimum_groups=3, minimum_selfcal_groups=50) print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either snowballs. if expand_large_events: From 89d3cb459ab127f243b076f3ba74f2dbb1d06c93 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 13:02:17 -0400 Subject: [PATCH 095/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index e7fd5997..7357913d 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -14,7 +14,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=30,): + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=30): """ Find CRs/Jumps in each integration within the input data array. The input @@ -34,7 +34,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, read_noise : float, 2D array The read noise of each pixel - rejection_thresh : float + normal_rej_thresh : float cosmic ray sigma rejection threshold two_diff_rej_thresh : float @@ -96,7 +96,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pixels above current row also to be flagged as a CR """ -# print("min groups ", minimum_groups ) + print("min self groups ", minimum_selfcal_groups) # copy data and group DQ array if copy_arrs: dataa = dataa.copy() @@ -251,8 +251,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh)) - log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' - log.info(log_str.format(len(row4cr), 'five or more')) +# log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' +# log.info(log_str.format(len(row4cr), 'five or more')) # get the rows, col pairs for all pixels with at least one CR all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) From d168a026c44bf355014d80958a7030ba5abe3484 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Sat, 27 May 2023 13:09:51 -0400 Subject: [PATCH 096/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 7357913d..4864a084 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -282,7 +282,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # or there is only one two diffs left (which means there is # actually one left, since the next CR will be masked after # checking that condition) - + ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ + sigma[np.newaxis, np.newaxis, :, :] while new_CR_found and ((ndiffs - np.sum(np.isnan(pix_first_diffs))) > 2): new_CR_found = False From c2482e388e9f24a53c599f9ac084665a4a7ce903 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 30 May 2023 17:10:09 -0400 Subject: [PATCH 097/196] group diffs --- src/stcal/jump/jump.py | 13 ++++++++---- src/stcal/jump/twopoint_difference.py | 29 ++++++++++++++------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index dd023f1a..f99ce924 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -257,7 +257,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, minimum_groups=3, minimum_selfcal_groups=50) - print("total primary CRs", total_primary_crs) # This is the flag that controls the flagging of either snowballs. if expand_large_events: flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, @@ -359,7 +358,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # This is the flag that controls the flagging of either # snowballs or showers. if expand_large_events: - flag_large_events(gdq, jump_flag, sat_flag, + total_snowballs = flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, min_jump_area=min_jump_area, expand_factor=expand_factor, @@ -367,6 +366,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, min_sat_radius_extend=min_sat_radius_extend, edge_size=edge_size, sat_expand=sat_expand, max_extended_radius=max_extended_radius) + log.info('Total snowballs = %i' % total_snowballs) + number_extended_events = total_snowballs if find_showers: gdq, num_showers = \ find_faint_extended(data, gdq, readnoise_2d, @@ -380,6 +381,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, ellipse_expand=extend_ellipse_expand_ratio, num_grps_masked=grps_masked_after_shower, max_extended_radius=max_extended_radius) + log.info('Total showers= %i' % num_showers) + number_extended_events = num_showers elapsed = time.time() - start log.info('Total elapsed time = %g sec' % elapsed) @@ -390,7 +393,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, readnoise_2d /= gain_2d # Return the updated data quality arrays - return gdq, pdq + return gdq, pdq, total_primary_crs, number_extended_events def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, @@ -445,6 +448,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, log.info('Flagging large Snowballs') n_showers_grp = [] + total_snowballs = 0 for integration in range(gdq.shape[0]): for group in range(1, gdq.shape[1]): current_gdq = 1.0 * gdq[integration, group, :, :] @@ -471,6 +475,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, else: snowballs = jump_ellipses n_showers_grp.append(len(snowballs)) + total_snowballs += len(snowballs) gdq, num_events = extend_ellipses(gdq, integration, group, snowballs, sat_flag, jump_flag, @@ -481,7 +486,7 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, else: log.info(f' In integration {integration}, number of snowballs ' + f'in each group = {n_showers_grp}') - + return total_snowballs def extend_saturation(cube, grp, sat_ellipses, sat_flag, min_sat_radius_extend, expansion=2, diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 4864a084..37cfda61 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -169,19 +169,21 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, - axis=(0, 1)) + # axis=(0, 1)) + axis=0) fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, - axis=(0, 1), masked=True) - max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) - min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) - delta_diff = max_diffs - min_diffs - rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) - avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) - fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) - out_rms = rms_diff.filled(fill_value=np.nan) - fits.writeto("rms_diff.fits", out_rms, overwrite=True) - out_diffs = delta_diff.filled(fill_value=np.nan) + # axis=(0, 1), masked=True) + axis=0, masked = True) + # max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) + # min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) + # delta_diff = max_diffs - min_diffs + # rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) + # avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) + # fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) + # out_rms = rms_diff.filled(fill_value=np.nan) + # fits.writeto("rms_diff.fits", out_rms, overwrite=True) + # out_diffs = delta_diff.filled(fill_value=np.nan) # jump_mask = 1.0 * clipped_diffs.mask - 1.0 * first_diffs_masked.mask jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) @@ -282,8 +284,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # or there is only one two diffs left (which means there is # actually one left, since the next CR will be masked after # checking that condition) - ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ - sigma[np.newaxis, np.newaxis, :, :] + while new_CR_found and ((ndiffs - np.sum(np.isnan(pix_first_diffs))) > 2): new_CR_found = False @@ -392,7 +393,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] =\ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - print("finish flag after jump, num crs =", num_primary_crs) + log.info("Total Primary CRs = %i", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs From 15f36ef00fbaa17254047b9e7bd07b81c8ae9e59 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 30 May 2023 17:17:30 -0400 Subject: [PATCH 098/196] Update jump.py --- src/stcal/jump/jump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index f99ce924..270b4e48 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -357,6 +357,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, print("total primary CRs, multiple threads", total_primary_crs) # This is the flag that controls the flagging of either # snowballs or showers. + number_extended_events = 0 if expand_large_events: total_snowballs = flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, From f22bc4961f0c842af010fa5fc072a77302c59fd2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 30 May 2023 17:26:53 -0400 Subject: [PATCH 099/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 270b4e48..8a152d60 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -199,6 +199,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, constants.update_dqflags(dqflags) # populate dq flags sat_flag = dqflags["SATURATED"] jump_flag = dqflags["JUMP_DET"] + number_extended_events = 0 # Flag the pixeldq where the gain is <=0 or NaN so they will be ignored wh_g = np.where(gain_2d <= 0.) if len(wh_g[0] > 0): @@ -357,7 +358,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, print("total primary CRs, multiple threads", total_primary_crs) # This is the flag that controls the flagging of either # snowballs or showers. - number_extended_events = 0 if expand_large_events: total_snowballs = flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, From 2aa01eca41edd9124863b257341d3568062fc216 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 30 May 2023 17:33:09 -0400 Subject: [PATCH 100/196] Update jump.py --- src/stcal/jump/jump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 8a152d60..2dc6dc52 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -394,7 +394,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, readnoise_2d /= gain_2d # Return the updated data quality arrays - return gdq, pdq, total_primary_crs, number_extended_events + return gdq, pdq + #return gdq, pdq, total_primary_crs, number_extended_events def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, From 742bac623f67994f75fd3a91ddc8d760e0347ffd Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 10:57:07 -0400 Subject: [PATCH 101/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 5765fa14..cbb38e15 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1063,4 +1063,23 @@ def test_first_grp_flag_issue(): after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) - fits.writeto("outgdq.fits",gdq, overwrite=True) \ No newline at end of file + fits.writeto("outgdq.fits",gdq, overwrite=True) + +def test_5grp_TSO(): + nints=20 + nrows = 2 + ncols = 2 + ngroups = 5 + readnoise = 25 + data = np.random.normal(0, readnoise, size=(nints, ngroups, nrows, ncols)) + read_noise = np.full((nrows, ncols), readnoise, dtype=np.float32) + gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint32) + + gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] + gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] + gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + after_jump_flag_e1=0.0, after_jump_flag_n1=0, + after_jump_flag_e2=0.0, after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) From 7a13b3f1634b8996e5713b5f56035555ba2ff22d Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 11:02:30 -0400 Subject: [PATCH 102/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 37cfda61..7e5152d2 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -371,28 +371,28 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, group, row, col + 1] & dnu_flag) == 0: gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) -# print("finish flag 4 neighbors") - # flag n groups after jumps above the specified thresholds to account for - # the transient seen after ramp jumps - for integ in range(nints): - flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] - flag_groups = [after_jump_flag_n1, after_jump_flag_n2] - e_jump_int = e_jump[integ, :, :, :] - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) - for cthres, cgroup in zip(flag_e_threshold, flag_groups): - if cgroup > 0: -# log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") - - for j in range(len(cr_group)): - group = cr_group[j] - row = cr_row[j] - col = cr_col[j] - if e_jump_int[group - 1, row, col] >= cthres[row, col]: - for kk in range(group, min(group + cgroup + 1, ngroups)): - if (gdq[integ, kk, row, col] & sat_flag) == 0: - if (gdq[integ, kk, row, col] & dnu_flag) == 0: - gdq[integ, kk, row, col] =\ - np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + + # flag n groups after jumps above the specified thresholds to account for + # the transient seen after ramp jumps + flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] + flag_groups = [after_jump_flag_n1, after_jump_flag_n2] + + cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + for cthres, cgroup in zip(flag_e_threshold, flag_groups): + if cgroup > 0: + log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") + + for j in range(len(cr_group)): + group = cr_group[j] + row = cr_row[j] + col = cr_col[j] + if e_jump[group - 1, row, col] >= cthres[row, col]: + for kk in range(group, min(group + cgroup + 1, ngroups)): + if (gdq[integ, kk, row, col] & sat_flag) == 0: + if (gdq[integ, kk, row, col] & dnu_flag) == 0: + gdq[integ, kk, row, col] = \ + np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + log.info("Total Primary CRs = %i", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs From 8068445ab8309e7f2c86c822bdaf166793a26e6c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 12:21:55 -0400 Subject: [PATCH 103/196] updates --- src/stcal/jump/twopoint_difference.py | 2 +- tests/test_twopoint_difference.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 7e5152d2..597c122f 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -371,7 +371,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, group, row, col + 1] & dnu_flag) == 0: gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) - + # flag n groups after jumps above the specified thresholds to account for # the transient seen after ramp jumps flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index cbb38e15..90868475 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1078,8 +1078,10 @@ def test_5grp_TSO(): gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] + data[0, :, 0, 0] =[21500, 37600, 52082, 65068, 58627] gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ - find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + find_crs(data, gdq, read_noise, 3, 4, 5, 1, True, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) + fits.writeto("new_gdq.fits", gdq, overwrite=True) \ No newline at end of file From ce458f58beb4a2077dc244ae1ecbe7af4e814583 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 13:04:55 -0400 Subject: [PATCH 104/196] updates --- src/stcal/jump/twopoint_difference.py | 48 +++++++++++++-------------- tests/test_twopoint_difference.py | 11 +++--- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 597c122f..e7cf4869 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -164,7 +164,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # if nints > 1: if total_groups >= minimum_selfcal_groups: - ratio = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ + ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) @@ -322,7 +322,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if flag_4_neighbors: # iterate over each 'jump' pixel for j in range(len(cr_group)): - ratio_this_pix = ratio[cr_integ[j], cr_group[j] - 1, cr_row[j], cr_col[j]] + ratio_this_pix = ratio_all[cr_integ[j], cr_group[j] - 1, cr_row[j], cr_col[j]] # Jumps must be in a certain range to have neighbors flagged if ratio_this_pix < max_jump_to_flag_neighbors and \ @@ -372,28 +372,28 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) - # flag n groups after jumps above the specified thresholds to account for - # the transient seen after ramp jumps - flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] - flag_groups = [after_jump_flag_n1, after_jump_flag_n2] - - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) - for cthres, cgroup in zip(flag_e_threshold, flag_groups): - if cgroup > 0: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") - - for j in range(len(cr_group)): - group = cr_group[j] - row = cr_row[j] - col = cr_col[j] - if e_jump[group - 1, row, col] >= cthres[row, col]: - for kk in range(group, min(group + cgroup + 1, ngroups)): - if (gdq[integ, kk, row, col] & sat_flag) == 0: - if (gdq[integ, kk, row, col] & dnu_flag) == 0: - gdq[integ, kk, row, col] = \ - np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - - log.info("Total Primary CRs = %i", num_primary_crs) + # flag n groups after jumps above the specified thresholds to account for + # the transient seen after ramp jumps + flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] + flag_groups = [after_jump_flag_n1, after_jump_flag_n2] + + cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + for cthres, cgroup in zip(flag_e_threshold, flag_groups): + if cgroup > 0: + log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") + + for j in range(len(cr_group)): + group = cr_group[j] + row = cr_row[j] + col = cr_col[j] + if e_jump[group - 1, row, col] >= cthres[row, col]: + for kk in range(group, min(group + cgroup + 1, ngroups)): + if (gdq[integ, kk, row, col] & sat_flag) == 0: + if (gdq[integ, kk, row, col] & dnu_flag) == 0: + gdq[integ, kk, row, col] = \ + np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + +# log.info("Total Primary CRs = %i", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 90868475..28f5561e 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1071,17 +1071,18 @@ def test_5grp_TSO(): ncols = 2 ngroups = 5 readnoise = 25 - data = np.random.normal(0, readnoise, size=(nints, ngroups, nrows, ncols)) + data = np.random.normal(0, 0.1 * readnoise, size=(nints, ngroups, nrows, ncols)) read_noise = np.full((nrows, ncols), readnoise, dtype=np.float32) gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint32) gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] - gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] +# gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] - data[0, :, 0, 0] =[21500, 37600, 52082, 65068, 58627] + data[0, :, 0, 0] = [21500, 37600, 52082, 65068, 58627] + data[0, :, 0, 1] = [21500, 37600, 52082, 65068, 58627] gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ - find_crs(data, gdq, read_noise, 3, 4, 5, 1, True, 1000, 10, DQFLAGS, + find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=5000) fits.writeto("new_gdq.fits", gdq, overwrite=True) \ No newline at end of file From b4d3ba0ab15c58c6af10f84969546a0b8f9c4ac8 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 14:28:11 -0400 Subject: [PATCH 105/196] new test --- src/stcal/jump/twopoint_difference.py | 4 ++-- tests/test_twopoint_difference.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index e7cf4869..78e2daf4 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -284,8 +284,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # or there is only one two diffs left (which means there is # actually one left, since the next CR will be masked after # checking that condition) - - while new_CR_found and ((ndiffs - np.sum(np.isnan(pix_first_diffs))) > 2): + unusable_diffs = np.sum(np.isnan(pix_first_diffs)) + while new_CR_found and ((ndiffs - unusable_diffs) > 2): new_CR_found = False diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 28f5561e..b60dc044 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1085,4 +1085,18 @@ def test_5grp_TSO(): after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=5000) - fits.writeto("new_gdq.fits", gdq, overwrite=True) \ No newline at end of file + fits.writeto("new_gdq.fits", gdq, overwrite=True) + +def test_5grp_realTSO(): + hdul = fits.open("obs2508_cutout_jump.fits") + gdq = hdul['groupdq'].data + data = hdul['sci'].data + readnoise = 25 + read_noise = np.full((3, 3), readnoise, dtype=np.float32) + + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + after_jump_flag_e1=0.0, after_jump_flag_n1=0, + after_jump_flag_e2=0.0, after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=15000) + fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) \ No newline at end of file From dce72857bb91a7096dbc147f4846eea0541c047b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 14:48:37 -0400 Subject: [PATCH 106/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 78e2daf4..add57ee0 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -372,26 +372,26 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, group, row, col + 1] =\ np.bitwise_or(gdq[integ, group, row, col + 1], jump_flag) - # flag n groups after jumps above the specified thresholds to account for - # the transient seen after ramp jumps - flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] - flag_groups = [after_jump_flag_n1, after_jump_flag_n2] - - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) - for cthres, cgroup in zip(flag_e_threshold, flag_groups): - if cgroup > 0: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") - - for j in range(len(cr_group)): - group = cr_group[j] - row = cr_row[j] - col = cr_col[j] - if e_jump[group - 1, row, col] >= cthres[row, col]: - for kk in range(group, min(group + cgroup + 1, ngroups)): - if (gdq[integ, kk, row, col] & sat_flag) == 0: - if (gdq[integ, kk, row, col] & dnu_flag) == 0: - gdq[integ, kk, row, col] = \ - np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + # flag n groups after jumps above the specified thresholds to account for + # the transient seen after ramp jumps + flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] + flag_groups = [after_jump_flag_n1, after_jump_flag_n2] + + cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + for cthres, cgroup in zip(flag_e_threshold, flag_groups): + if cgroup > 0: + log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") + + for j in range(len(cr_group)): + group = cr_group[j] + row = cr_row[j] + col = cr_col[j] + if e_jump[group - 1, row, col] >= cthres[row, col]: + for kk in range(group, min(group + cgroup + 1, ngroups)): + if (gdq[integ, kk, row, col] & sat_flag) == 0: + if (gdq[integ, kk, row, col] & dnu_flag) == 0: + gdq[integ, kk, row, col] = \ + np.bitwise_or(gdq[integ, kk, row, col], jump_flag) # log.info("Total Primary CRs = %i", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs From 138f5923b1528b01161111c6d104e0b9b978d0e3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 14:52:07 -0400 Subject: [PATCH 107/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 2dc6dc52..61e7f1bb 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -35,7 +35,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, extend_inner_radius=1, extend_outer_radius=2.6, extend_ellipse_expand_ratio=1.2, grps_masked_after_shower=5, max_extended_radius=200, minimum_groups=3, - minimum_selfcal_groups=50): + minimum_selfcal_groups=5000): """ This is the high-level controlling routine for the jump detection process. From 3bb29e68215da2c4179c8ce3f29eb5870306c2b2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 31 May 2023 14:58:47 -0400 Subject: [PATCH 108/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 61e7f1bb..0ffc5f28 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -257,7 +257,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, - minimum_groups=3, minimum_selfcal_groups=50) + minimum_groups=3, minimum_selfcal_groups=minimum_selfcal_groups) # This is the flag that controls the flagging of either snowballs. if expand_large_events: flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, From 33305da368ae3152ef316bed016dbe1d4f7e1394 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 1 Jun 2023 10:00:21 -0400 Subject: [PATCH 109/196] change to nints for min groups --- src/stcal/jump/jump.py | 2 +- src/stcal/jump/twopoint_difference.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 0ffc5f28..3b985819 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -35,7 +35,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, extend_inner_radius=1, extend_outer_radius=2.6, extend_ellipse_expand_ratio=1.2, grps_masked_after_shower=5, max_extended_radius=200, minimum_groups=3, - minimum_selfcal_groups=5000): + minimum_selfcal_groups=100): """ This is the high-level controlling routine for the jump detection process. diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index add57ee0..f155b078 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -163,7 +163,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, e_jump = first_diffs - median_diffs[np.newaxis, :, :] # if nints > 1: - if total_groups >= minimum_selfcal_groups: + if nints >= minimum_selfcal_groups: ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( From 0e3476e09d67a3287d7bfaa52ed4327443b7ff24 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 1 Jun 2023 14:17:21 -0400 Subject: [PATCH 110/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index b60dc044..52c16e76 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1099,4 +1099,18 @@ def test_5grp_realTSO(): after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=15000) - fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) \ No newline at end of file + fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) + +def test_5grp_allTSO(): + hdul = fits.open("obs2508_sigmaclip_base_dark_current.fits") + gdq = hdul['groupdq'].data + data = hdul['sci'].data + readnoise = 25 + read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) + + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + after_jump_flag_e1=0.0, after_jump_flag_n1=0, + after_jump_flag_e2=0.0, after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=15000) + fits.writeto("new_sigma_clip_base.fits", gdq, overwrite=True) \ No newline at end of file From 02e648b9e97ded940fe23e56a27ed627355b34d2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 7 Jun 2023 14:21:08 -0400 Subject: [PATCH 111/196] fixes --- src/stcal/jump/jump.py | 4 ++-- src/stcal/jump/twopoint_difference.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 3b985819..fbcae79d 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -394,8 +394,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, readnoise_2d /= gain_2d # Return the updated data quality arrays - return gdq, pdq - #return gdq, pdq, total_primary_crs, number_extended_events +# return gdq, pdq + return gdq, pdq, total_primary_crs, number_extended_events def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index f155b078..94ef1b5b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -162,10 +162,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # first_diffs from first_diffs, take the abs. value and divide by sigma. e_jump = first_diffs - median_diffs[np.newaxis, :, :] # if nints > 1: - - if nints >= minimum_selfcal_groups: - ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ + ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] + if nints >= minimum_selfcal_groups: log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, From e5c98b67fa51065ec39d63338023a46939ed3a9a Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 7 Jun 2023 15:30:37 -0400 Subject: [PATCH 112/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 94ef1b5b..5bbb5b29 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -393,7 +393,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_or(gdq[integ, kk, row, col], jump_flag) # log.info("Total Primary CRs = %i", num_primary_crs) - return gdq, row_below_gdq, row_above_gdq, num_primary_crs + return gdq, row_below_gdq, row_above_gdq, num_primary_crs, num_flagged_grps def calc_med_first_diffs(first_diffs): From ca559c0d00a6a2d146466e5e163092f16c6c4f00 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 8 Jun 2023 10:30:38 -0400 Subject: [PATCH 113/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 5bbb5b29..94ef1b5b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -393,7 +393,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_or(gdq[integ, kk, row, col], jump_flag) # log.info("Total Primary CRs = %i", num_primary_crs) - return gdq, row_below_gdq, row_above_gdq, num_primary_crs, num_flagged_grps + return gdq, row_below_gdq, row_above_gdq, num_primary_crs def calc_med_first_diffs(first_diffs): From a8a35bfd28a87fd95a87a52cccacd40c714cdfc3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Jun 2023 16:28:59 -0400 Subject: [PATCH 114/196] update --- src/stcal/jump/jump.py | 11 ++-- src/stcal/jump/twopoint_difference.py | 78 ++++++++++----------------- tests/test_twopoint_difference.py | 12 ++--- 3 files changed, 41 insertions(+), 60 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index fbcae79d..e5adc071 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -35,7 +35,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, extend_inner_radius=1, extend_outer_radius=2.6, extend_ellipse_expand_ratio=1.2, grps_masked_after_shower=5, max_extended_radius=200, minimum_groups=3, - minimum_selfcal_groups=100): + minimum_sigclip_groups=100, only_use_ints=True): """ This is the high-level controlling routine for the jump detection process. @@ -257,7 +257,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, after_jump_flag_n1=after_jump_flag_n1, after_jump_flag_e2=after_jump_flag_e2, after_jump_flag_n2=after_jump_flag_n2, copy_arrs=False, - minimum_groups=3, minimum_selfcal_groups=minimum_selfcal_groups) + minimum_groups=3, minimum_sigclip_groups=minimum_sigclip_groups, + only_use_ints=only_use_ints) # This is the flag that controls the flagging of either snowballs. if expand_large_events: flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=min_sat_area, @@ -303,7 +304,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, min_jump_to_flag_neighbors, dqflags, after_jump_flag_e1, after_jump_flag_n1, after_jump_flag_e2, after_jump_flag_n2, - copy_arrs, minimum_groups, minimum_selfcal_groups)) + copy_arrs, minimum_groups, minimum_sigclip_groups, + only_use_ints)) # last slice get the rest slices.insert(n_slices - 1, (data[:, :, (n_slices - 1) * @@ -319,7 +321,8 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, min_jump_to_flag_neighbors, dqflags, after_jump_flag_e1, after_jump_flag_n1, after_jump_flag_e2, after_jump_flag_n2, - copy_arrs, minimum_groups, minimum_selfcal_groups)) + copy_arrs, minimum_groups, minimum_sigclip_groups, + only_use_ints)) log.info("Creating %d processes for jump detection " % n_slices) pool = multiprocessing.Pool(processes=n_slices) # Starts each slice in its own process. Starmap allows more than one diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 94ef1b5b..606c8637 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -1,9 +1,9 @@ import logging import numpy as np import astropy.stats as stats +from astropy.io import fits log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -from astropy.io import fits def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, @@ -14,7 +14,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=30): + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=100, + only_use_ints=True): """ Find CRs/Jumps in each integration within the input data array. The input @@ -96,7 +97,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pixels above current row also to be flagged as a CR """ - print("min self groups ", minimum_selfcal_groups) # copy data and group DQ array if copy_arrs: dataa = dataa.copy() @@ -109,7 +109,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # get readnoise, squared read_noise_2 = read_noise**2 - # create arrays for output row_above_gdq = np.zeros((nints, ngroups, ncols), dtype=np.uint8) row_below_gdq = np.zeros((nints, ngroups, ncols), dtype=np.uint8) @@ -127,12 +126,13 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 - total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps -# print("test total_groups", total_groups, "minimum groups", minimum_groups, "seflcal min groups", -# minimum_selfcal_groups) + if only_use_ints: + total_groups = dat.shape[0] + else: + total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps if total_groups < minimum_groups: log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") - return gdq, row_below_gdq, row_above_gdq + return gdq, row_below_gdq, row_above_gdq, 0 else: # set 'saturated' or 'do not use' pixels to nan in data @@ -148,7 +148,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) -# fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) + fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) # fits.writeto("median_diffs.fits", median_diffs, overwrite=True) # calculate sigma for each pixel @@ -164,43 +164,28 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # if nints > 1: ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] - if nints >= minimum_selfcal_groups: + if (only_use_ints and nints >= minimum_sigclip_groups) or \ + (not only_use_ints and total_groups >= minimum_sigclip_groups): log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( - str(total_groups), str(minimum_selfcal_groups), str(normal_rej_thresh))) - mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, - # axis=(0, 1)) - axis=0) - fits.writeto('stddev_sigclip.fits', stddev, overwrite=True) - clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, - # axis=(0, 1), masked=True) - axis=0, masked = True) - # max_diffs = np.nanmax(clipped_diffs, axis=(0, 1)) - # min_diffs = np.nanmin(clipped_diffs, axis=(0, 1)) - # delta_diff = max_diffs - min_diffs - # rms_diff = np.nanstd(clipped_diffs, axis=(0, 1)) - # avg_diff = np.nanmean(clipped_diffs, axis=(0, 1)) - # fits.writeto("avg_diff.fits", avg_diff.filled(fill_value=np.nan), overwrite=True) - # out_rms = rms_diff.filled(fill_value=np.nan) - # fits.writeto("rms_diff.fits", out_rms, overwrite=True) - # out_diffs = delta_diff.filled(fill_value=np.nan) - # jump_mask = 1.0 * clipped_diffs.mask - 1.0 * first_diffs_masked.mask + str(total_groups), str(minimum_sigclip_groups), str(normal_rej_thresh))) + if only_use_ints: + mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, + axis=0) + fits.writeto("stddev.fits", stddev, overwrite=True) + clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, + axis=0, masked=True) + else: + mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, + axis=(0, 1)) + fits.writeto("stddev.fits", stddev, overwrite=True) + clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, + axis=(0, 1), masked=True) jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) - fits.writeto('mask_of_first_diffs.fits', 1.0*first_diffs_masked.mask, overwrite=True) - fits.writeto('mask_of_clipped_diffs.fits', 1.0*clipped_diffs.mask, overwrite=True) - fits.writeto("jump_mask.fits", jump_mask * 4.0, overwrite=True) - trimmed_mask = jump_mask[:, 4:-4, :, :] -# print(trimmed_mask[0:300,:, 0, 0]) -# print("total masked pixels", np.sum(trimmed_mask), "total Pixels", trimmed_mask.shape[0]*trimmed_mask.shape[1]*trimmed_mask.shape[2]* -# trimmed_mask.shape[3]) -# print("done") jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False - # fits.writeto("jump_mask2.fits", jump_mask * 1.0, overwrite=True) - fits.writeto("incoming_gdq.fits", gdq, overwrite=True) gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - fits.writeto("new_gdq.fits", gdq, overwrite=True) # if grp is all jump set to do not use for integ in range(dat.shape[0]): for grp in range(dat.shape[1]): @@ -208,7 +193,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 - fits.writeto("new_gdq2.fits", gdq, overwrite=True) else: for integ in range(nints): @@ -229,7 +213,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - + if integ == 5: + fits.writeto("sigma.fits", sigma, overwrite=True) # reset sigma so pxels with 0 readnoise are not flagged as jumps sigma[np.where(sigma == 0.)] = np.nan @@ -252,9 +237,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh)) -# log_str = 'From highest outlier, two-point found {} pixels with at least one CR from {} groups.' -# log.info(log_str.format(len(row4cr), 'five or more')) - # get the rows, col pairs for all pixels with at least one CR all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) @@ -315,7 +297,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) - # print("start flag 4 neighbors part") cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel @@ -324,8 +305,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, ratio_this_pix = ratio_all[cr_integ[j], cr_group[j] - 1, cr_row[j], cr_col[j]] # Jumps must be in a certain range to have neighbors flagged - if ratio_this_pix < max_jump_to_flag_neighbors and \ - ratio_this_pix > min_jump_to_flag_neighbors: + if (ratio_this_pix < max_jump_to_flag_neighbors) and \ + (ratio_this_pix > min_jump_to_flag_neighbors): integ = cr_integ[j] group = cr_group[j] row = cr_row[j] @@ -379,8 +360,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - log.info(f"Flagging {cgroup} groups after detected jumps with e >= {np.mean(cthres)}.") - for j in range(len(cr_group)): group = cr_group[j] row = cr_row[j] @@ -392,7 +371,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, kk, row, col] = \ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) -# log.info("Total Primary CRs = %i", num_primary_crs) return gdq, row_below_gdq, row_above_gdq, num_primary_crs diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 52c16e76..83f16f10 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1102,15 +1102,15 @@ def test_5grp_realTSO(): fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) def test_5grp_allTSO(): - hdul = fits.open("obs2508_sigmaclip_base_dark_current.fits") + hdul = fits.open("obs2508_noshower_sigclip_base_00_dark_current.fits") gdq = hdul['groupdq'].data - data = hdul['sci'].data - readnoise = 25 + data = hdul['sci'].data * 5.5 + readnoise = 5.5 * 6 read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ - find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, + find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=15000) - fits.writeto("new_sigma_clip_base.fits", gdq, overwrite=True) \ No newline at end of file + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) + fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) \ No newline at end of file From a35e3705ea3421888ab3432455bea9962eced0a6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Jun 2023 16:36:16 -0400 Subject: [PATCH 115/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 606c8637..d2042691 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -132,6 +132,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps if total_groups < minimum_groups: log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") + log.info("Total Groups {}".format(str(total_groups))) return gdq, row_below_gdq, row_above_gdq, 0 else: From e71554b41f7643f9f55bc4b68e4ca97a5eafc119 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Jun 2023 16:45:32 -0400 Subject: [PATCH 116/196] update --- src/stcal/jump/jump.py | 1 + src/stcal/jump/twopoint_difference.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index e5adc071..b35a7fd0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -196,6 +196,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, pdq : int, 2D array updated pixel dq array """ + print("only use ints detect jumps", only_use_ints) constants.update_dqflags(dqflags) # populate dq flags sat_flag = dqflags["SATURATED"] jump_flag = dqflags["JUMP_DET"] diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index d2042691..dcc2f3da 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -106,7 +106,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape ndiffs = ngroups - 1 - + print("only use ints twopoint",only_use_ints) # get readnoise, squared read_noise_2 = read_noise**2 # create arrays for output From cb1c83d0446c08997c8e01b028658175d8c64c45 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Jun 2023 16:47:52 -0400 Subject: [PATCH 117/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index dcc2f3da..f242cd76 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -126,6 +126,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 + print("dat shape", dat.shape) if only_use_ints: total_groups = dat.shape[0] else: From f66a49f1b83a17576c50fb63174b496a8af00986 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Jun 2023 17:06:00 -0400 Subject: [PATCH 118/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index f242cd76..67f59ee1 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -127,13 +127,16 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 print("dat shape", dat.shape) - if only_use_ints: + print() + if only_use_ints and dat.shape[0]: total_groups = dat.shape[0] else: total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps - if total_groups < minimum_groups: + if (dat.shape[1] < minimum_groups and only_use_ints and dat.shape[0] < minimum_sigclip_groups) or \ + (not only_use_ints and dat.shape[0] * dat.shape[1] < minimum_sigclip_groups and + dat.shape[1] < minimum_groups): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") - log.info("Total Groups {}".format(str(total_groups))) + log.info("Data shape {}".format(str(dat.shape))) return gdq, row_below_gdq, row_above_gdq, 0 else: From 318b6916d28ae93e9fac596565972009d88efaa2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 13 Jun 2023 14:39:30 -0400 Subject: [PATCH 119/196] update --- src/stcal/jump/twopoint_difference.py | 13 +++++++++---- tests/test_twopoint_difference.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 67f59ee1..81edbdc6 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -127,7 +127,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 print("dat shape", dat.shape) - print() if only_use_ints and dat.shape[0]: total_groups = dat.shape[0] else: @@ -252,7 +251,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # to the threshold to determine if another CR can be flagged and clipped. # repeat this process until no more CRs are found. for j in range(len(all_crs_row)): - + if j == 2724: + row = all_crs_row[j] + col = all_crs_col[j] + test = first_diffs[:, all_crs_row[j], all_crs_col[j]] # get arrays of abs(diffs), ratio, readnoise for this pixel pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] @@ -292,11 +294,14 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, rej_thresh = three_diff_rej_thresh if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + if np.isnan(new_pix_ratio).all(): + print("all nan") + else: + new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: new_CR_found = True pix_cr_mask[new_pix_max_ratio_idx] = 0 - + unusable_diffs = np.sum(np.isnan(pix_first_diffs)) # Found all CRs for this pix - set flags in input DQ array gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 83f16f10..e8721662 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1108,6 +1108,21 @@ def test_5grp_allTSO(): readnoise = 5.5 * 6 read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, + after_jump_flag_e1=0.0, after_jump_flag_n1=0, + after_jump_flag_e2=0.0, after_jump_flag_n2=0, + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) + fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) + + +def test_1059(): + hdul = fits.open("data/nircam_1059_00_dark_current.fits") + gdq = hdul['groupdq'].data + data = hdul['sci'].data * 5.5 + readnoise = 5.5 * 6 + read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) + gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, From 77f82c3d4688f7556a70776ebbf6533ab34b3feb Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 13 Jun 2023 14:45:52 -0400 Subject: [PATCH 120/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 81edbdc6..638d8e5c 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -272,8 +272,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # or there is only one two diffs left (which means there is # actually one left, since the next CR will be masked after # checking that condition) - unusable_diffs = np.sum(np.isnan(pix_first_diffs)) - while new_CR_found and ((ndiffs - unusable_diffs) > 2): + while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): new_CR_found = False From 83499491e19578f8a364d9aa95fdbfcdd598381f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 13 Jun 2023 17:20:00 -0400 Subject: [PATCH 121/196] cleanup --- src/stcal/jump/jump.py | 9 -- src/stcal/jump/twopoint_difference.py | 13 +-- tests/test_twopoint_difference.py | 114 +++++++++++++------------- 3 files changed, 58 insertions(+), 78 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index b35a7fd0..8e7881ab 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -359,7 +359,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # save the neighbors to be flagged that will be in the next slice previous_row_above_gdq = row_above_gdq.copy() k += 1 - print("total primary CRs, multiple threads", total_primary_crs) # This is the flag that controls the flagging of either # snowballs or showers. if expand_large_events: @@ -683,7 +682,6 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, Total number of showers detected. """ - print("find showers on sigmaclip") read_noise_2 = readnoise_2d**2 data = indata.copy() data[gdq == sat_flag] = np.nan @@ -720,10 +718,6 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, ratio.shape[2]), dtype=np.uint8) exty, extx = np.where(masked_smoothed_ratio > snr_threshold) extended_emission[exty, extx] = 1 - if grp == 179 and intg == 0: - fits.writeto("masked_ratio.fits",masked_ratio, overwrite=True) - fits.writeto("masked_smoothed_ratio.fits", masked_smoothed_ratio, overwrite=True) - fits.writeto("extended_emission.fits", extended_emission, overwrite=True) # find the contours of the extended emission contours, hierarchy = cv.findContours(extended_emission, cv.RETR_EXTERNAL, @@ -743,8 +737,6 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, image2 = np.zeros_like(image) cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), -1) big_contour_image = image2[:, :, 2] - fits.writeto("big_contour.fits", big_contour_image, overwrite=True) - fits.writeto("image2.fits", image2, overwrite=True) num_ellipses = len(ellipses) for ellipse in ellipses: ceny = ellipse[0][0] @@ -773,7 +765,6 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, round(axis2 / 2)), alpha, 0, 360, (0, 0, jump_flag), -1) jump_ellipse = image[:, :, 2] - fits.writeto("jump_ellipse.fits", jump_ellipse, overwrite=True) if len(ellipses) > 0: # add all the showers for this integration to the list all_ellipses.append([intg, grp, ellipses]) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 638d8e5c..2752a9a8 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -106,7 +106,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape ndiffs = ngroups - 1 - print("only use ints twopoint",only_use_ints) # get readnoise, squared read_noise_2 = read_noise**2 # create arrays for output @@ -126,7 +125,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 - print("dat shape", dat.shape) if only_use_ints and dat.shape[0]: total_groups = dat.shape[0] else: @@ -217,8 +215,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - if integ == 5: - fits.writeto("sigma.fits", sigma, overwrite=True) # reset sigma so pxels with 0 readnoise are not flagged as jumps sigma[np.where(sigma == 0.)] = np.nan @@ -251,10 +247,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # to the threshold to determine if another CR can be flagged and clipped. # repeat this process until no more CRs are found. for j in range(len(all_crs_row)): - if j == 2724: - row = all_crs_row[j] - col = all_crs_col[j] - test = first_diffs[:, all_crs_row[j], all_crs_col[j]] # get arrays of abs(diffs), ratio, readnoise for this pixel pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] @@ -293,10 +285,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, rej_thresh = three_diff_rej_thresh if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: rej_thresh = two_diff_rej_thresh - if np.isnan(new_pix_ratio).all(): - print("all nan") - else: - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: new_CR_found = True pix_cr_mask[new_pix_max_ratio_idx] = 0 diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e8721662..18aa1802 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -29,7 +29,7 @@ def _cube(ngroups, readnoise=10): def test_nocrs_noflux(setup_cube): ngroups = 5 data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -42,7 +42,7 @@ def test_5grps_cr3_noflux(setup_cube): data[0, 0:2, 100, 100] = 10.0 data[0, 2:5, 100, 100] = 1000 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -55,7 +55,7 @@ def test_5grps_cr2_noflux(setup_cube): data[0, 0, 100, 100] = 10.0 data[0, 1:6, 100, 100] = 1000 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -72,7 +72,7 @@ def test_6grps_negative_differences_zeromedian(setup_cube): data[0, 3, 100, 100] = 105 data[0, 4, 100, 100] = 100 data[0, 5, 100, 100] = 100 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # no CR was found @@ -84,7 +84,7 @@ def test_5grps_cr2_negjumpflux(setup_cube): data[0, 0, 100, 100] = 1000.0 data[0, 1:6, 100, 100] = 10 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -96,7 +96,7 @@ def test_3grps_cr2_noflux(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) data[0, 0, 100, 100] = 10.0 data[0, 1:4, 100, 100] = 1000 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -109,7 +109,7 @@ def test_4grps_cr2_noflux(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) data[0, 0, 100, 100] = 10.0 data[0, 1:4, 100, 100] = 1000 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -125,7 +125,7 @@ def test_5grps_cr2_nframe2(setup_cube): data[0, 2, 100, 100] = 1002 data[0, 3, 100, 100] = 1001 data[0, 4, 100, 100] = 1005 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -141,7 +141,7 @@ def test_4grps_twocrs_2nd_4th(setup_cube): data[0, 1, 100, 100] = 60 data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(np.max(out_gdq) == 4) # a CR was found @@ -156,7 +156,7 @@ def test_5grps_twocrs_2nd_5th(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -172,7 +172,7 @@ def test_5grps_twocrs_2nd_5thbig(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 2115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -193,7 +193,7 @@ def test_10grps_twocrs_2nd_8th_big(setup_cube): data[0, 7, 100, 100] = 2115 data[0, 8, 100, 100] = 2115 data[0, 9, 100, 100] = 2115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -214,7 +214,7 @@ def test_10grps_twocrs_10percenthit(setup_cube): data[0:200, 7, 100, 100] = 2115 data[0:200, 8, 100, 100] = 2115 data[0:200, 9, 100, 100] = 2115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -230,7 +230,7 @@ def test_5grps_twocrs_2nd_5thbig_nframes2(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 2115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -247,7 +247,7 @@ def test_6grps_twocrs_2nd_5th(setup_cube): data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 data[0, 5, 100, 100] = 115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -264,7 +264,7 @@ def test_6grps_twocrs_2nd_5th_nframes2(setup_cube): data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 data[0, 5, 100, 100] = 115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -287,7 +287,7 @@ def test_6grps_twocrs_twopixels_nframes2(setup_cube): data[0, 3, 200, 100] = 60 data[0, 4, 200, 100] = 115 data[0, 5, 200, 100] = 115 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -304,7 +304,7 @@ def test_5grps_cr2_negslope(setup_cube): data[0, 2, 100, 100] = -200 data[0, 3, 100, 100] = -260 data[0, 4, 100, 100] = -360 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -321,7 +321,7 @@ def test_6grps_1cr(setup_cube): data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 1146 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (4 == out_gdq[0, 5, 100, 100]) @@ -338,7 +338,7 @@ def test_7grps_1cr(setup_cube): data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 60 data[0, 6, 100, 100] = 1160 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -356,7 +356,7 @@ def test_8grps_1cr(setup_cube): data[0, 5, 100, 100] = 60 data[0, 6, 100, 100] = 1160 data[0, 7, 100, 100] = 1175 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -376,7 +376,7 @@ def test_9grps_1cr_1sat(setup_cube): data[0, 7, 100, 100] = 1175 data[0, 8, 100, 100] = 6175 gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -398,7 +398,7 @@ def test_10grps_1cr_2sat(setup_cube): data[0, 9, 100, 100] = 6175 gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 9, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -422,7 +422,7 @@ def test_11grps_1cr_3sat(setup_cube): gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 9, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -446,7 +446,7 @@ def test_11grps_0cr_3donotuse(setup_cube): gdq[0, 0, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 9, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 10, 100, 100] = DQFLAGS['DO_NOT_USE'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 0, 0, 0], out_gdq[0, 1:-2, 100, 100])) @@ -461,7 +461,7 @@ def test_5grps_nocr(setup_cube): data[0, 2, 100, 100] = 21 data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -476,7 +476,7 @@ def test_6grps_nocr(setup_cube): data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 60 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -488,7 +488,7 @@ def test_10grps_cr2_gt3sigma(setup_cube): nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -502,7 +502,7 @@ def test_10grps_cr2_3sigma_nocr(setup_cube): nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # a CR was found @@ -516,7 +516,7 @@ def test_10grps_cr2_gt3sigma_2frames(setup_cube): nframes = 2 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -530,7 +530,7 @@ def test_10grps_cr2_gt3sigma_2frames_offdiag(setup_cube): nframes = 2 data[0, 0, 100, 110] = 0 data[0, 1:11, 100, 110] = crmag - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -544,7 +544,7 @@ def test_10grps_cr2_3sigma_2frames_nocr(setup_cube): nframes = 2 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # a CR was found @@ -560,7 +560,7 @@ def test_10grps_nocr_2pixels_sigma0(setup_cube): data[0, 1:11, 100, 100] = crmag read_noise[50, 50] = 0.0 read_noise[60, 60] = 0.0 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # no CR was found @@ -577,7 +577,7 @@ def test_5grps_satat4_crat3(setup_cube): data[0, 4, 100, 100] = 61000 gdq[0, 3, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 4, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -604,7 +604,7 @@ def test_6grps_satat6_crat1(setup_cube): data[0, 4, 100, 101] = 30010 data[0, 5, 100, 101] = 35015 gdq[0, 5, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -630,7 +630,7 @@ def test_6grps_satat6_crat1_flagadjpixels(setup_cube): data[0, 4, 100, 101] = 30010 data[0, 5, 100, 101] = 35015 gdq[0, 5, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -652,7 +652,7 @@ def test_10grps_satat8_crsat3and6(setup_cube): data[0, 6, 100, 100] = 45000 data[0, 7:11, 100, 100] = 61000 gdq[0, 7:11, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -677,7 +677,7 @@ def test_median_with_saturation(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:10, 100, 100] = 60000 gdq[0, 7:10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 0, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -698,7 +698,7 @@ def test_median_with_saturation_even_num_sat_frames(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:10, 100, 100] = 60000 gdq[0, 6:10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 2, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -719,7 +719,7 @@ def test_median_with_saturation_odd_number_final_difference(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:9, 100, 100] = 60000 gdq[0, 6:9, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -747,7 +747,7 @@ def test_first_last_group(setup_cube): gdq[0, 0, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 6, 100, 100] = DQFLAGS['DO_NOT_USE'] - outgdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -765,7 +765,7 @@ def test_2group(setup_cube): # set groups 1,2 - to be around 30,000 data[0, 1, 0, 0] = 30000.0 - outgdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert outgdq[0, 1, 0, 0] == 0 @@ -782,7 +782,7 @@ def test_4group(setup_cube): data[0, 2, 0, 0] = 30020.0 data[0, 3, 0, 0] = 30000.0 - outgdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert outgdq[0, 1, 0, 0] == 4 @@ -802,7 +802,7 @@ def test_first_last_4group(setup_cube): # treat as MIRI data with first and last flagged gdq[0, 0, :, :] = DQFLAGS['DO_NOT_USE'] gdq[0, 3, :, :] = DQFLAGS['DO_NOT_USE'] - outgdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -824,7 +824,7 @@ def test_first_last_3group(setup_cube): data[0, 2, 0, 0] = 30020.0 gdq[0, 2, 0, 0] = DQFLAGS['DO_NOT_USE'] # only flag the last group - outgdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -849,7 +849,7 @@ def test_10grps_1cr_afterjump(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -875,7 +875,7 @@ def test_10grps_1cr_afterjump_2group(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -906,7 +906,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 10000.0 - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -933,7 +933,7 @@ def test_10grps_1cr_afterjump_twothresholds(setup_cube): after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 500. after_jump_flag_e2 = np.full(data.shape[2:4], 1.0) * 10. - out_gdq, row_below_gdq, row_above_gdq = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -1025,8 +1025,8 @@ def test_sigma_clip(): hdul = fits.open('lrs_TSOjump_sigmaclip5_00_refpix.fits') data = hdul['SCI'].data * 4.0 gdq = hdul['GROUPDQ'].data - indata = data[:14, :, :, :] - ingdq = gdq[:14, :, :, :] + indata = data[:53, :, :, :] + ingdq = gdq[:53, :, :, :] read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() @@ -1038,7 +1038,7 @@ def test_sigma_clip(): after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50,) + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=50,) fits.writeto("outgdq.fits", gdq, overwrite=True) print('done') @@ -1062,7 +1062,7 @@ def test_first_grp_flag_issue(): find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=50) + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=50) fits.writeto("outgdq.fits",gdq, overwrite=True) def test_5grp_TSO(): @@ -1084,7 +1084,7 @@ def test_5grp_TSO(): find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=5000) + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=5000) fits.writeto("new_gdq.fits", gdq, overwrite=True) def test_5grp_realTSO(): @@ -1098,9 +1098,9 @@ def test_5grp_realTSO(): find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_selfcal_groups=15000) + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) - +@pytest.mark.skip("Used for local testing") def test_5grp_allTSO(): hdul = fits.open("obs2508_noshower_sigclip_base_00_dark_current.fits") gdq = hdul['groupdq'].data @@ -1115,7 +1115,7 @@ def test_5grp_allTSO(): copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) - +@pytest.mark.skip("Used for local testing") def test_1059(): hdul = fits.open("data/nircam_1059_00_dark_current.fits") gdq = hdul['groupdq'].data From 81e2562f43f17738298e3cfa57de47de2151c4e9 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 13 Jun 2023 17:47:28 -0400 Subject: [PATCH 122/196] Update jump.py --- src/stcal/jump/jump.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 8e7881ab..7b9cbcae 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -196,7 +196,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, pdq : int, 2D array updated pixel dq array """ - print("only use ints detect jumps", only_use_ints) constants.update_dqflags(dqflags) # populate dq flags sat_flag = dqflags["SATURATED"] jump_flag = dqflags["JUMP_DET"] From 0054573f20402b01cb8323d4b93cf4ca36bccb62 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Jun 2023 09:58:31 -0400 Subject: [PATCH 123/196] remove fits --- src/stcal/jump/jump.py | 1 - src/stcal/jump/twopoint_difference.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 7b9cbcae..54f21eff 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -1,7 +1,6 @@ import logging import multiprocessing import time -from astropy.io import fits import numpy as np import cv2 as cv diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 2752a9a8..146b5668 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -1,7 +1,6 @@ import logging import numpy as np import astropy.stats as stats -from astropy.io import fits log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -146,13 +145,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) -# fits.writeto("first_diffs.fits", first_diffs, overwrite=True) # calc. the median of first_diffs for each pixel along the group axis first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - fits.writeto("first_diffs_masked.fits", first_diffs_masked.filled(fill_value=np.nan), overwrite=True) median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) -# fits.writeto("median_diffs.fits", median_diffs, overwrite=True) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -173,13 +169,11 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if only_use_ints: mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=0) - fits.writeto("stddev.fits", stddev, overwrite=True) clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=0, masked=True) else: mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1)) - fits.writeto("stddev.fits", stddev, overwrite=True) clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1), masked=True) jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) From c6ad3a76b22769593e3ea25c79eaa9fa9e0a9d3e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Jun 2023 13:40:34 -0400 Subject: [PATCH 124/196] add stddev output --- src/stcal/jump/jump.py | 18 ++++++++++++++++-- src/stcal/jump/twopoint_difference.py | 11 +++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 54f21eff..659ae0b0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -247,7 +247,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, n_slices = max_available if n_slices == 1: - gdq, row_below_dq, row_above_dq, total_primary_crs = \ + gdq, row_below_dq, row_above_dq, total_primary_crs, stddev = \ twopt.find_crs(data, gdq, readnoise_2d, rejection_thresh, three_grp_thresh, four_grp_thresh, frames_per_group, flag_4_neighbors, max_jump_to_flag_neighbors, @@ -334,11 +334,25 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # Reconstruct gdq, the row_above_gdq, and the row_below_gdq from the # slice result total_primary_crs = 0 + if only_use_ints: + stddev = np.zeros((gdq.shape[1]- 1, gdq.shape[2], gdq.shape[3]), + dtype=np.float32) + else: + stddev = np.zeros((gdq.shape[2], gdq.shape[3]), + dtype=np.float32) for resultslice in real_result: if len(real_result) == k + 1: # last result gdq[:, :, k * yinc:n_rows, :] = resultslice[0] + if only_use_ints: + stddev[:, k * yinc:n_rows, :] = resultslice[4] + else: + stddev[k * yinc:n_rows, :] = resultslice[4] else: gdq[:, :, k * yinc:(k + 1) * yinc, :] = resultslice[0] + if only_use_ints: + stddev[:, k * yinc:(k + 1) * yinc, :] = resultslice[4] + else: + stddev[k * yinc:(k + 1) * yinc, :] = resultslice[4] row_below_gdq[:, :, :] = resultslice[1] row_above_gdq[:, :, :] = resultslice[2] total_primary_crs += resultslice[3] @@ -396,7 +410,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # Return the updated data quality arrays # return gdq, pdq - return gdq, pdq, total_primary_crs, number_extended_events + return gdq, pdq, total_primary_crs, number_extended_events, stddev def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 146b5668..74431030 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -362,8 +362,15 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] = \ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - - return gdq, row_below_gdq, row_above_gdq, num_primary_crs + if stddev in locals(): + return gdq, row_below_gdq, row_above_gdq, num_primary_crs, stddev + else: + if only_use_ints: + dummy = np.zeros((dat.shape[1]-1, dat.shape[2], dat.shape[3]), + dtype=np.float32) + else: + dummy = np.zeros((dat.shape[2], dat.shape[3]), dtype=np.float32) + return gdq, row_below_gdq, row_above_gdq, num_primary_crs, dummy def calc_med_first_diffs(first_diffs): From a70b7f66c624028b999d1871484fd09fb6b393f5 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Jun 2023 14:05:12 -0400 Subject: [PATCH 125/196] updates --- src/stcal/jump/jump.py | 2 +- src/stcal/jump/twopoint_difference.py | 6 +++--- tests/test_twopoint_difference.py | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 659ae0b0..c64efe9d 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -335,7 +335,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # slice result total_primary_crs = 0 if only_use_ints: - stddev = np.zeros((gdq.shape[1]- 1, gdq.shape[2], gdq.shape[3]), + stddev = np.zeros((gdq.shape[1] - 1, gdq.shape[2], gdq.shape[3]), dtype=np.float32) else: stddev = np.zeros((gdq.shape[2], gdq.shape[3]), diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 74431030..340a9454 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -362,14 +362,14 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, if (gdq[integ, kk, row, col] & dnu_flag) == 0: gdq[integ, kk, row, col] = \ np.bitwise_or(gdq[integ, kk, row, col], jump_flag) - if stddev in locals(): + if 'stddev' in locals(): return gdq, row_below_gdq, row_above_gdq, num_primary_crs, stddev else: if only_use_ints: - dummy = np.zeros((dat.shape[1]-1, dat.shape[2], dat.shape[3]), + dummy = np.zeros((dataa.shape[1]-1, dataa.shape[2], dataa.shape[3]), dtype=np.float32) else: - dummy = np.zeros((dat.shape[2], dat.shape[3]), dtype=np.float32) + dummy = np.zeros((dataa.shape[2], dataa.shape[3]), dtype=np.float32) return gdq, row_below_gdq, row_above_gdq, num_primary_crs, dummy diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 18aa1802..31b82ae0 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -918,7 +918,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): def test_10grps_1cr_afterjump_twothresholds(setup_cube): ngroups = 10 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10) + data, gdq, nframes, read_noise, rej_threshold, stddev = setup_cube(ngroups, readnoise=10) nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1, 100, 100] = 10 @@ -1030,7 +1030,7 @@ def test_sigma_clip(): read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 gain = np.ones_like(indata) * 4.0 hdul.close() - gdq, row_below_gdq, row_above_gdq, total_primary_crs = find_crs(indata, ingdq, read_noise, 3, + gdq, row_below_gdq, row_above_gdq, total_primary_crs, stddev = find_crs(indata, ingdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, @@ -1074,13 +1074,14 @@ def test_5grp_TSO(): data = np.random.normal(0, 0.1 * readnoise, size=(nints, ngroups, nrows, ncols)) read_noise = np.full((nrows, ncols), readnoise, dtype=np.float32) gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint32) - + np.expand_dims(gdq, axis=0) + np.expand_dims(data, axis=0) gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] # gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] data[0, :, 0, 0] = [21500, 37600, 52082, 65068, 58627] data[0, :, 0, 1] = [21500, 37600, 52082, 65068, 58627] - gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + gdq, row_below_gdq, row_above_gdq, total_primary_crs, stddev = \ find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, From 857a11b18fdd2bab810302b622da5ad24389f400 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Jun 2023 17:28:59 -0400 Subject: [PATCH 126/196] test updates --- src/stcal/jump/twopoint_difference.py | 4 +- tests/test_jump.py | 149 +++++--------------------- tests/test_twopoint_difference.py | 110 +++++++++---------- 3 files changed, 83 insertions(+), 180 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 340a9454..40e5eac5 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -133,7 +133,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, dat.shape[1] < minimum_groups): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") log.info("Data shape {}".format(str(dat.shape))) - return gdq, row_below_gdq, row_above_gdq, 0 + dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), + dtype=np.float32) + return gdq, row_below_gdq, row_above_gdq, 0, dummy else: # set 'saturated' or 'do not use' pixels to nan in data diff --git a/tests/test_jump.py b/tests/test_jump.py index 3f927ea1..292dd50a 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -2,18 +2,11 @@ import pytest from astropy.io import fits -from stcal.jump.jump import flag_large_events, find_circles, find_ellipses, extend_saturation, \ - point_inside_ellipse, point_inside_rectangle, flag_large_events, detect_jumps +from stcal.jump.jump import flag_large_events, find_ellipses, extend_saturation, \ + point_inside_ellipse, find_faint_extended DQFLAGS = {'JUMP_DET': 4, 'SATURATED': 2, 'DO_NOT_USE': 1, 'GOOD': 0, 'NO_GAIN_VALUE': 8} -try: - import cv2 as cv # noqa: F401 - - OPENCV_INSTALLED = True -except ImportError: - OPENCV_INSTALLED = False - @pytest.fixture(scope='function') def setup_cube(): @@ -49,13 +42,16 @@ def test_find_simple_ellipse(): def test_find_ellipse2(): plane = np.zeros(shape=(5, 5), dtype=np.uint8) - - plane[1,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'],DQFLAGS['JUMP_DET'], 0] - plane[2,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] - plane[3,:] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] - ellipse = find_ellipses(plane, DQFLAGS['JUMP_DET'], 1) - print(ellipse) - assert ellipse == 1 + plane[1, :] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] + plane[2, :] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] + plane[3, :] = [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], 0] + ellipses = find_ellipses(plane, DQFLAGS['JUMP_DET'], 1) + ellipse = ellipses[0] + assert ellipse[0][0] == 2 + assert ellipse[0][1] == 2 + assert ellipse[1][0] == 2 + assert ellipse[1][1] == 2 + assert ellipse[2] == 90.0 def test_extend_saturation_simple(): @@ -68,21 +64,6 @@ def test_extend_saturation_simple(): cube[1, 4, 3] = DQFLAGS['SATURATED'] cube[1, 3, 2] = DQFLAGS['SATURATED'] cube[1, 2, 2] = DQFLAGS['JUMP_DET'] - fits.writeto("start_sat_extend.fits", cube, overwrite=True) - sat_circles = find_circles(cube[grp, :, :], DQFLAGS['SATURATED'], 1) - new_cube = extend_saturation(cube, grp, sat_circles, DQFLAGS['SATURATED'], DQFLAGS['JUMP_DET'], - min_sat_radius_extend, expansion=1) - assert cube[grp, 2, 2] == DQFLAGS['SATURATED'] - assert cube[grp, 3, 5] == DQFLAGS['SATURATED'] - assert cube[grp, 3, 6] == 0 - fits.writeto("out_sat_extend.fits", cube, overwrite=True) - - - -def test_flag_large_events(): - cube = np.zeros(shape=(1, 5, 7, 7), dtype=np.uint8) - grp = 1 - min_sat_radius_extend = 1 sat_circles = find_ellipses(cube[grp, :, :], DQFLAGS['SATURATED'], 1) new_cube = extend_saturation(cube, grp, sat_circles, DQFLAGS['SATURATED'], min_sat_radius_extend, expansion=1.1) @@ -100,7 +81,6 @@ def test_flag_large_events_nosnowball(): cube[0, 1, 3, 4] = DQFLAGS['SATURATED'] cube[0, 1, 4, 3] = DQFLAGS['SATURATED'] cube[0, 1, 3, 2] = DQFLAGS['SATURATED'] - # cross of saturation surrounding by jump -> snowball but sat core is not new # should have no snowball trigger cube[0, 2, 3, 3] = DQFLAGS['SATURATED'] @@ -225,7 +205,8 @@ def test_inside_ellipse4(): ellipse = ((0, 0), (1, 2), 0) point = (1, 0.5) result = point_inside_ellipse(point, ellipse) - assert result + assert not result + def test_inside_ellipes5(): point = (1110.5, 870.5) @@ -233,71 +214,6 @@ def test_inside_ellipes5(): result = point_inside_ellipse(point, ellipse) assert result -def test_plane23(): - incube = fits.getdata('input_jump_cube.fits') - testcube = incube[:, 22:24, :, :] - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube23.fits", testcube, overwrite=True) - -def test_plane13(): - incube = fits.getdata('input_jump_cube.fits') - testcube = incube[:, 13:15, :, :] - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube13.fits", testcube, overwrite=True) - - -def test_5580_plane8(): - incube = fits.getdata('input5580_jump_cube.fits') - testcube = incube[:, 2:5, :, :] - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=6, - expand_factor=2.0, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube8.fits", testcube, overwrite=True) - -def test_2333_plane25(): - incube = fits.getdata('input_jump_cube23-33.fits') - testcube = incube[:, 0:3, :, :] - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=7, - expand_factor=2.5, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) - -def test_edgeflage_130140(): - incube = fits.getdata('input_jump_cube130140.fits') - testcube = incube[:, :-1, :, :] - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=7, - expand_factor=2.5, use_ellipses=False, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=3) - fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) - -def test_miri_input(): - incube = fits.getdata('input_jump_cube_miri_01.fits') - testcube = incube[:, 1:5, :, :] - testcube = incube - - flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, - min_jump_area=7, - expand_factor=2.5, use_ellipses=True, - sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=3) - fits.writeto("output_jump_cube_miri.fits", testcube, overwrite=True) - -def test_inputjumpall(): - testcube = fits.getdata('input_jump_cube.fits') - @pytest.mark.skip("Fails in CI") def test_inputjumpall(): @@ -319,29 +235,14 @@ def test_inputjump_sat_star(): min_jump_area=6, expand_factor=2.0, use_ellipses=False, sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) - fits.writeto("output_jump_cube2.fits", testcube, overwrite=True) - - -def test_detect_jumps_runaway(): - testcube = fits.getdata('smalldark2232_00_dark_current.fits') - hdl = fits.open('smalldark2232_00_dark_current.fits') - gdq = hdl['GROUPDQ'].data - pdq = hdl['pixeldq'].data - err = np.ones_like(pdq).astype('float64') - gain_2d = fits.getdata('jwst_nirspec_gain_0023.fits') - readnoise_2d = fits.getdata('jwst_nirspec_readnoise_0038.fits') - - detect_jumps(1, testcube, gdq, pdq, err, - gain_2d, readnoise_2d, 4, - 5, 6, 'half', 1000, - 10, True, DQFLAGS, - after_jump_flag_dn1=0.0, - after_jump_flag_n1=0, - after_jump_flag_dn2=0.0, - after_jump_flag_n2=0, - min_sat_area=1, - min_jump_area=5, - expand_factor=2.5, - use_ellipses=False, - sat_required_snowball=True, - expand_large_events=True) + fits.writeto("outgdq2.fits", testcube, overwrite=True) + + +@pytest.mark.skip("Used for local testing") +def test_inputjump_sat_star2(): + testcube = fits.getdata('input_gdq_satstar.fits') + flag_large_events(testcube, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], min_sat_area=1, + min_jump_area=6, + expand_factor=2.0, use_ellipses=False, + sat_required_snowball=True, min_sat_radius_extend=2.5, sat_expand=2) + fits.writeto("outgdq_satstar.fits", testcube, overwrite=True) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 31b82ae0..ef6a7e79 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -29,7 +29,7 @@ def _cube(ngroups, readnoise=10): def test_nocrs_noflux(setup_cube): ngroups = 5 data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -42,7 +42,7 @@ def test_5grps_cr3_noflux(setup_cube): data[0, 0:2, 100, 100] = 10.0 data[0, 2:5, 100, 100] = 1000 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -55,7 +55,7 @@ def test_5grps_cr2_noflux(setup_cube): data[0, 0, 100, 100] = 10.0 data[0, 1:6, 100, 100] = 1000 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -72,7 +72,7 @@ def test_6grps_negative_differences_zeromedian(setup_cube): data[0, 3, 100, 100] = 105 data[0, 4, 100, 100] = 100 data[0, 5, 100, 100] = 100 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # no CR was found @@ -84,7 +84,7 @@ def test_5grps_cr2_negjumpflux(setup_cube): data[0, 0, 100, 100] = 1000.0 data[0, 1:6, 100, 100] = 10 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -96,7 +96,7 @@ def test_3grps_cr2_noflux(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) data[0, 0, 100, 100] = 10.0 data[0, 1:4, 100, 100] = 1000 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -109,7 +109,7 @@ def test_4grps_cr2_noflux(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) data[0, 0, 100, 100] = 10.0 data[0, 1:4, 100, 100] = 1000 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -125,7 +125,7 @@ def test_5grps_cr2_nframe2(setup_cube): data[0, 2, 100, 100] = 1002 data[0, 3, 100, 100] = 1001 data[0, 4, 100, 100] = 1005 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -141,7 +141,7 @@ def test_4grps_twocrs_2nd_4th(setup_cube): data[0, 1, 100, 100] = 60 data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(np.max(out_gdq) == 4) # a CR was found @@ -156,7 +156,7 @@ def test_5grps_twocrs_2nd_5th(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -172,7 +172,7 @@ def test_5grps_twocrs_2nd_5thbig(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 2115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -193,7 +193,7 @@ def test_10grps_twocrs_2nd_8th_big(setup_cube): data[0, 7, 100, 100] = 2115 data[0, 8, 100, 100] = 2115 data[0, 9, 100, 100] = 2115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -214,7 +214,7 @@ def test_10grps_twocrs_10percenthit(setup_cube): data[0:200, 7, 100, 100] = 2115 data[0:200, 8, 100, 100] = 2115 data[0:200, 9, 100, 100] = 2115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -230,7 +230,7 @@ def test_5grps_twocrs_2nd_5thbig_nframes2(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 2115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -247,7 +247,7 @@ def test_6grps_twocrs_2nd_5th(setup_cube): data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 data[0, 5, 100, 100] = 115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -264,7 +264,7 @@ def test_6grps_twocrs_2nd_5th_nframes2(setup_cube): data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 data[0, 5, 100, 100] = 115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -287,7 +287,7 @@ def test_6grps_twocrs_twopixels_nframes2(setup_cube): data[0, 3, 200, 100] = 60 data[0, 4, 200, 100] = 115 data[0, 5, 200, 100] = 115 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -304,7 +304,7 @@ def test_5grps_cr2_negslope(setup_cube): data[0, 2, 100, 100] = -200 data[0, 3, 100, 100] = -260 data[0, 4, 100, 100] = -360 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -321,7 +321,7 @@ def test_6grps_1cr(setup_cube): data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 1146 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (4 == out_gdq[0, 5, 100, 100]) @@ -338,7 +338,7 @@ def test_7grps_1cr(setup_cube): data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 60 data[0, 6, 100, 100] = 1160 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -356,7 +356,7 @@ def test_8grps_1cr(setup_cube): data[0, 5, 100, 100] = 60 data[0, 6, 100, 100] = 1160 data[0, 7, 100, 100] = 1175 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -376,7 +376,7 @@ def test_9grps_1cr_1sat(setup_cube): data[0, 7, 100, 100] = 1175 data[0, 8, 100, 100] = 6175 gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -398,7 +398,7 @@ def test_10grps_1cr_2sat(setup_cube): data[0, 9, 100, 100] = 6175 gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 9, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -422,7 +422,7 @@ def test_11grps_1cr_3sat(setup_cube): gdq[0, 8, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 9, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == out_gdq[0, 6, 100, 100]) @@ -446,7 +446,7 @@ def test_11grps_0cr_3donotuse(setup_cube): gdq[0, 0, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 9, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 10, 100, 100] = DQFLAGS['DO_NOT_USE'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 0, 0, 0], out_gdq[0, 1:-2, 100, 100])) @@ -461,7 +461,7 @@ def test_5grps_nocr(setup_cube): data[0, 2, 100, 100] = 21 data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -476,7 +476,7 @@ def test_6grps_nocr(setup_cube): data[0, 3, 100, 100] = 33 data[0, 4, 100, 100] = 46 data[0, 5, 100, 100] = 60 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -488,7 +488,7 @@ def test_10grps_cr2_gt3sigma(setup_cube): nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -502,7 +502,7 @@ def test_10grps_cr2_3sigma_nocr(setup_cube): nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # a CR was found @@ -516,7 +516,7 @@ def test_10grps_cr2_gt3sigma_2frames(setup_cube): nframes = 2 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -530,7 +530,7 @@ def test_10grps_cr2_gt3sigma_2frames_offdiag(setup_cube): nframes = 2 data[0, 0, 100, 110] = 0 data[0, 1:11, 100, 110] = crmag - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found @@ -544,7 +544,7 @@ def test_10grps_cr2_3sigma_2frames_nocr(setup_cube): nframes = 2 data[0, 0, 100, 100] = 0 data[0, 1:11, 100, 100] = crmag - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # a CR was found @@ -560,7 +560,7 @@ def test_10grps_nocr_2pixels_sigma0(setup_cube): data[0, 1:11, 100, 100] = crmag read_noise[50, 50] = 0.0 read_noise[60, 60] = 0.0 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(0 == np.max(out_gdq)) # no CR was found @@ -577,7 +577,7 @@ def test_5grps_satat4_crat3(setup_cube): data[0, 4, 100, 100] = 61000 gdq[0, 3, 100, 100] = DQFLAGS['SATURATED'] gdq[0, 4, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -604,7 +604,7 @@ def test_6grps_satat6_crat1(setup_cube): data[0, 4, 100, 101] = 30010 data[0, 5, 100, 101] = 35015 gdq[0, 5, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -630,7 +630,7 @@ def test_6grps_satat6_crat1_flagadjpixels(setup_cube): data[0, 4, 100, 101] = 30010 data[0, 5, 100, 101] = 35015 gdq[0, 5, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -652,7 +652,7 @@ def test_10grps_satat8_crsat3and6(setup_cube): data[0, 6, 100, 100] = 45000 data[0, 7:11, 100, 100] = 61000 gdq[0, 7:11, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found @@ -677,7 +677,7 @@ def test_median_with_saturation(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:10, 100, 100] = 60000 gdq[0, 7:10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 0, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -698,7 +698,7 @@ def test_median_with_saturation_even_num_sat_frames(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:10, 100, 100] = 60000 gdq[0, 6:10, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 2, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -719,7 +719,7 @@ def test_median_with_saturation_odd_number_final_difference(setup_cube): data[0, 7, 100, 100] = 49900 data[0, 8:9, 100, 100] = 60000 gdq[0, 6:9, 100, 100] = DQFLAGS['SATURATED'] - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert (np.array_equal([0, 0, 0, 0, 0, 4, 2, 2, 2], out_gdq[0, :, 100, 100])) @@ -747,7 +747,7 @@ def test_first_last_group(setup_cube): gdq[0, 0, 100, 100] = DQFLAGS['DO_NOT_USE'] gdq[0, 6, 100, 100] = DQFLAGS['DO_NOT_USE'] - outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -765,7 +765,7 @@ def test_2group(setup_cube): # set groups 1,2 - to be around 30,000 data[0, 1, 0, 0] = 30000.0 - outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert outgdq[0, 1, 0, 0] == 0 @@ -782,7 +782,7 @@ def test_4group(setup_cube): data[0, 2, 0, 0] = 30020.0 data[0, 3, 0, 0] = 30000.0 - outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert outgdq[0, 1, 0, 0] == 4 @@ -802,7 +802,7 @@ def test_first_last_4group(setup_cube): # treat as MIRI data with first and last flagged gdq[0, 0, :, :] = DQFLAGS['DO_NOT_USE'] gdq[0, 3, :, :] = DQFLAGS['DO_NOT_USE'] - outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -824,7 +824,7 @@ def test_first_last_3group(setup_cube): data[0, 2, 0, 0] = 30020.0 gdq[0, 2, 0, 0] = DQFLAGS['DO_NOT_USE'] # only flag the last group - outgdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + outgdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -849,7 +849,7 @@ def test_10grps_1cr_afterjump(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -875,7 +875,7 @@ def test_10grps_1cr_afterjump_2group(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -906,7 +906,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): data[0, 9, 100, 100] = 1209 after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 10000.0 - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -918,7 +918,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): def test_10grps_1cr_afterjump_twothresholds(setup_cube): ngroups = 10 - data, gdq, nframes, read_noise, rej_threshold, stddev = setup_cube(ngroups, readnoise=10) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10) nframes = 1 data[0, 0, 100, 100] = 0 data[0, 1, 100, 100] = 10 @@ -933,7 +933,7 @@ def test_10grps_1cr_afterjump_twothresholds(setup_cube): after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 500. after_jump_flag_e2 = np.full(data.shape[2:4], 1.0) * 10. - out_gdq, row_below_gdq, rows_above_gdq, total_crs = find_crs(data, gdq, read_noise, rej_threshold, + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, after_jump_flag_e1=after_jump_flag_e1, @@ -1019,7 +1019,7 @@ def test_median_func(): arr = np.zeros(4 * 2 * 2).reshape(4, 2, 2) arr[:, 0, 0] = np.array([-1., -2., np.nan, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == -1 - +@pytest.mark.skip("Used for local testing") def test_sigma_clip(): # hdul = fits.open('TSOjump_sc__refpix.fits') hdul = fits.open('lrs_TSOjump_sigmaclip5_00_refpix.fits') @@ -1058,7 +1058,7 @@ def test_first_grp_flag_issue(): gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] - gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, @@ -1095,7 +1095,7 @@ def test_5grp_realTSO(): readnoise = 25 read_noise = np.full((3, 3), readnoise, dtype=np.float32) - gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ find_crs(data, gdq, read_noise, 3, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, @@ -1109,7 +1109,7 @@ def test_5grp_allTSO(): readnoise = 5.5 * 6 read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) - gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, @@ -1124,7 +1124,7 @@ def test_1059(): readnoise = 5.5 * 6 read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) - gdq, row_below_gdq, row_above_gdq, total_primary_crs = \ + gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, From 90e0612a8d35987885ac5296ffe044357f3496d8 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 12:43:53 -0400 Subject: [PATCH 127/196] Reduce log output --- src/stcal/jump/jump.py | 10 ---------- src/stcal/ramp_fitting/ols_fit.py | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index c64efe9d..4dc416ee 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -498,11 +498,6 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, sat_flag, jump_flag, expansion=expand_factor, max_extended_radius=max_extended_radius) - if np.all(np.array(n_showers_grp) == 0): - log.info(f'No snowballs found in integration {integration}.') - else: - log.info(f' In integration {integration}, number of snowballs ' + - f'in each group = {n_showers_grp}') return total_snowballs def extend_saturation(cube, grp, sat_ellipses, sat_flag, @@ -794,9 +789,4 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, expand_by_ratio=True, num_grps_masked=num_grps_masked, max_extended_radius=max_extended_radius) - if np.all(all_ellipses == 0): - log.info('No showers found in exposure.') - else: - num_showers = len(all_ellipses) - log.info(f' Number of showers flagged = {num_showers}') return gdq, len(all_ellipses) diff --git a/src/stcal/ramp_fitting/ols_fit.py b/src/stcal/ramp_fitting/ols_fit.py index 083c58d0..8e65065a 100644 --- a/src/stcal/ramp_fitting/ols_fit.py +++ b/src/stcal/ramp_fitting/ols_fit.py @@ -1135,11 +1135,11 @@ def ramp_fit_compute_variances(ramp_data, gain_2d, readnoise_2d, fit_slopes_ans) # variance is proportional to the median estimated slope) to # outrageously large values so that they will have negligible # contributions. - var_p4[num_int, :, :, :] *= (segs_4[num_int, :, :, :] > 0) # Suppress, then re-enable harmless arithmetic warnings warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + var_p4[num_int, :, :, :] *= (segs_4[num_int, :, :, :] > 0) var_p4[var_p4 <= 0.] = utils.LARGE_VARIANCE var_r4[num_int, :, :, :] *= (segs_4[num_int, :, :, :] > 0) From 146798d38d66294343b590f829a50f730a8c5365 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 13:10:39 -0400 Subject: [PATCH 128/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 40e5eac5..8a8910ad 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -1,6 +1,7 @@ import logging import numpy as np import astropy.stats as stats +import warnings log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -221,8 +222,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] # create a 2d array containing the value of the largest 'ratio' for each group + warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) max_ratio = np.nanmax(ratio, axis=0) - + warnings.resetwarnings() # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) From 3f712da7874ec47ccc3aeeebd468fe99d41c481d Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 13:46:24 -0400 Subject: [PATCH 129/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 8a8910ad..949db672 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -169,6 +169,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, (not only_use_ints and total_groups >= minimum_sigclip_groups): log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_sigclip_groups), str(normal_rej_thresh))) + warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) if only_use_ints: mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=0) From e14eb59cac9380aefa363eadb39209b17da545cd Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 13:51:06 -0400 Subject: [PATCH 130/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 949db672..30389cb6 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -170,6 +170,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_sigclip_groups), str(normal_rej_thresh))) warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*Mean of empty slice.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*Degrees of freedom <= 0.*", RuntimeWarning) + if only_use_ints: mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=normal_rej_thresh, axis=0) From 1204c56e1c9a23c85f2c060484fd8344c474d316 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 13:56:39 -0400 Subject: [PATCH 131/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 30389cb6..51c72f14 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -196,6 +196,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 + warnings.resetwarnings() else: for integ in range(nints): From f5e5a0b4c0142c9e96f14c12635a16a3e060fca2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 14:07:38 -0400 Subject: [PATCH 132/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 51c72f14..a0d9d5fe 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -167,7 +167,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, sigma[np.newaxis, np.newaxis, :, :] if (only_use_ints and nints >= minimum_sigclip_groups) or \ (not only_use_ints and total_groups >= minimum_sigclip_groups): - log.info(" Jump Step using selfcal sigma clip {} greater than {}, rejection threshold {}".format( + log.info(" Jump Step using sigma clip {} greater than {}, rejection threshold {}".format( str(total_groups), str(minimum_sigclip_groups), str(normal_rej_thresh))) warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*Mean of empty slice.*", RuntimeWarning) From 7522f41e74db89b5b4d3e78273e6b3499cfaf861 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 14:24:44 -0400 Subject: [PATCH 133/196] Update jump.py --- src/stcal/jump/jump.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 4dc416ee..cb7c965a 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -3,6 +3,7 @@ import time import numpy as np import cv2 as cv +from astropy.io import fits from astropy.convolution import Ring2DKernel from astropy.convolution import convolve @@ -409,7 +410,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, readnoise_2d /= gain_2d # Return the updated data quality arrays -# return gdq, pdq + fits.writeto("stddev.fits", stddev, overwrite=True) return gdq, pdq, total_primary_crs, number_extended_events, stddev From ed147f651ab3c2eba786e434bea4f6973808343b Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Jun 2023 15:37:50 -0400 Subject: [PATCH 134/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index ef6a7e79..40af51a5 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1040,7 +1040,6 @@ def test_sigma_clip(): after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=50,) fits.writeto("outgdq.fits", gdq, overwrite=True) - print('done') def test_first_grp_flag_issue(): nints = 8 From db5b29e09714930101cb4c1c2b10d9952b3624f4 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 11:26:52 -0400 Subject: [PATCH 135/196] Add sigma clip to shower detection --- src/stcal/jump/jump.py | 38 +++++++++++++++++++++++++------------- tests/test_jump.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index cb7c965a..0d23bce0 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -4,6 +4,7 @@ import numpy as np import cv2 as cv from astropy.io import fits +import astropy.stats as stats from astropy.convolution import Ring2DKernel from astropy.convolution import convolve @@ -270,7 +271,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, max_extended_radius=max_extended_radius) if find_showers: gdq, num_showers = find_faint_extended(data, gdq, readnoise_2d, - frames_per_group, + frames_per_group, minimum_sigclip_groups, snr_threshold=extend_snr_threshold, min_shower_area=extend_min_area, inner=extend_inner_radius, @@ -388,7 +389,7 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, if find_showers: gdq, num_showers = \ find_faint_extended(data, gdq, readnoise_2d, - frames_per_group, + frames_per_group, minimum_sigclip_groups, snr_threshold=extend_snr_threshold, min_shower_area=extend_min_area, inner=extend_inner_radius, @@ -647,7 +648,8 @@ def near_edge(jump, low_threshold, high_threshold): return False -def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, +def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_groups, + snr_threshold=1.3, min_shower_area=40, inner=1, outer=2, sat_flag=2, jump_flag=4, ellipse_expand=1.1, num_grps_masked=25, max_extended_radius=200): @@ -696,21 +698,31 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, snr_threshold=1.3, data[gdq == 1] = np.nan data[gdq == jump_flag] = np.nan all_ellipses = [] + first_diffs = np.diff(data, axis=1) + first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) + if data.shape[0] > minimum_sigclip_groups: + mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=5, + axis=0) for intg in range(data.shape[0]): - diff = np.diff(data[intg], axis=0) - median_diffs = np.nanmedian(diff, axis=0) # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - - # The difference from the median difference for each group - e_jump = diff - median_diffs[np.newaxis, :, :] - - ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] # SNR ratio of - # each diff. + if data.shape[0] <= minimum_sigclip_groups: + median_diffs = np.nanmedian(first_diffs_masked[intg], axis=0) + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + # The difference from the median difference for each group + e_jump = first_diffs_masked[intg] - median_diffs[np.newaxis, :, :] + # SNR ratio of each diff. + ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] # The convolution kernal creation ring_2D_kernel = Ring2DKernel(inner, outer) - for grp in range(1, ratio.shape[0] + 1): + for grp in range(1, data.shape[1]): + if data.shape[0] > minimum_sigclip_groups: + median_diffs = median[grp-1] + sigma = stddev[grp-1] + # The difference from the median difference for each group + e_jump = first_diffs_masked[intg] - median_diffs[np.newaxis, :, :] + # SNR ratio of each diff. + ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] masked_ratio = ratio[grp-1].copy() jumpy, jumpx = np.where(gdq[intg, grp, :, :] == jump_flag) # mask pix. that are already flagged as jump diff --git a/tests/test_jump.py b/tests/test_jump.py index 292dd50a..ddfcbed1 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -155,7 +155,7 @@ def test_find_faint_extended(): rng = np.random.default_rng(12345) data[0, 1:, 14:20, 15:20] = 6 * gain * 1.7 data = data + rng.normal(size=(nint, ngrps, nrows, ncols)) * readnoise - gdq, num_showers = find_faint_extended(data, gdq, readnoise, 1, + gdq, num_showers = find_faint_extended(data, gdq, readnoise, 1, 100, snr_threshold=1.3, min_shower_area=20, inner=1, outer=2, sat_flag=2, jump_flag=4, @@ -193,6 +193,40 @@ def test_find_faint_extended(): # Check that the flags are not applied in the 3rd group after the event assert (np.all(gdq[0, 4, 12:22, 14:23]) == 0) +# No shower is found because the event is identical in all ints +def test_find_faint_extended_sigclip(): + nint, ngrps, ncols, nrows = 101, 6, 30, 30 + data = np.zeros(shape=(nint, ngrps, nrows, ncols), dtype=np.float32) + gdq = np.zeros_like(data, dtype=np.uint8) + gain = 4 + readnoise = np.ones(shape=(nrows, ncols), dtype=np.float32) * 6.0 * gain + rng = np.random.default_rng(12345) + data[0, 1:, 14:20, 15:20] = 6 * gain * 1.7 + data = data + rng.normal(size=(nint, ngrps, nrows, ncols)) * readnoise + gdq, num_showers = find_faint_extended(data, gdq, readnoise, 1, 100, + snr_threshold=1.3, + min_shower_area=20, inner=1, + outer=2, sat_flag=2, jump_flag=4, + ellipse_expand=1.1, num_grps_masked=3) + # Check that all the expected samples in group 2 are flagged as jump and + # that they are not flagged outside + assert (np.all(gdq[0, 1, 22, 14:23] == 0)) + assert (np.all(gdq[0, 1, 21, 16:20] == 0)) + assert (np.all(gdq[0, 1, 20, 15:22] == 0)) + assert (np.all(gdq[0, 1, 19, 15:23] == 0)) + assert (np.all(gdq[0, 1, 18, 14:23] == 0)) + assert (np.all(gdq[0, 1, 17, 14:23] == 0)) + assert (np.all(gdq[0, 1, 16, 14:23] == 0)) + assert (np.all(gdq[0, 1, 15, 14:22] == 0)) + assert (np.all(gdq[0, 1, 14, 16:22] == 0)) + assert (np.all(gdq[0, 1, 13, 17:21] == 0)) + assert (np.all(gdq[0, 1, 12, 14:23] == 0)) + assert (np.all(gdq[0, 1, 12:23, 24] == 0)) + assert (np.all(gdq[0, 1, 12:23, 13] == 0)) + + # Check that the flags are not applied in the 3rd group after the event + assert (np.all(gdq[0, 4, 12:22, 14:23]) == 0) + def test_inside_ellipse5(): ellipse = ((0, 0), (1, 2), -10) From eebbacf71563643ae2a18b1ed056631c00d86cb3 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 12:33:03 -0400 Subject: [PATCH 136/196] remove or mark as skip extraneous tests --- tests/test_ramp_fitting.py | 30 +----------------------------- tests/test_twopoint_difference.py | 6 +++--- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/tests/test_ramp_fitting.py b/tests/test_ramp_fitting.py index 7da74d76..52148efa 100644 --- a/tests/test_ramp_fitting.py +++ b/tests/test_ramp_fitting.py @@ -1194,35 +1194,7 @@ def test_new_saturation(): [[0.03073696, 0. , 0.]]]) np.testing.assert_allclose(cerr, check, tol, tol) -def test_contiguous_jumps(): -# hdul = fits.open('/users/mregan/downloads/dark_imager_wthfaint_1_jump.fits') - hdul = fits.open('TSOjump_sc__jump.fits') -# hdul = fits.open('/users/mregan/downloads/dark_imager_baseline_2_00_jump.fits') - jumpdiff = np.diff(hdul['SCI'].data, axis=1) - data = hdul['SCI'].data - gdq = hdul['groupdq'].data - ypix = 342 - xpix = 37 - indata = data[:, :, ypix:ypix+2, xpix:xpix+2] - indataraw = data[0, :, ypix:ypix+2, xpix:xpix+2] - ingdq = gdq[:, :, ypix:ypix+2, xpix:xpix+2] - stx1 = 1 - -# ingdq[135] = 125 - nints, ngroups, nrows, ncols = indata.shape[0], indata.shape[1], 2, 2 - rnoise_val, gain_val = 100000, 1 - nframes, gtime, dtime = 1, 2.77, 2.77 - dims = (nints, ngroups, nrows, ncols) - var = (rnoise_val, gain_val) - tm = (nframes, gtime, dtime) - ramp_data, rnoise, gain = setup_inputs(dims, var, tm) - ramp_data.data[:, :, :, :] = indata - ramp_data.groupdq[:, :, :, :] = ingdq - buffsize, save_opt, algo, wt, ncores = 512, True, "OLS", "optimal", "none" - slopes, cube, optional, gls_dummy = ramp_fit_data( - ramp_data, buffsize, save_opt, rnoise, gain, algo, wt, ncores, dqflags) - fits.writeto("slopes.fits", slopes[0], overwrite=True) - print(slopes) + def create_blank_ramp_data(dims, var, tm): """ Create empty RampData classes, as well as gain and read noise arrays, diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 40af51a5..e38c075c 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1040,7 +1040,7 @@ def test_sigma_clip(): after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=50,) fits.writeto("outgdq.fits", gdq, overwrite=True) - +@pytest.mark.skip("Used for local testing") def test_first_grp_flag_issue(): nints = 8 nrows = 2 @@ -1063,7 +1063,7 @@ def test_first_grp_flag_issue(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=50) fits.writeto("outgdq.fits",gdq, overwrite=True) - +@pytest.mark.skip("Used for local testing") def test_5grp_TSO(): nints=20 nrows = 2 @@ -1086,7 +1086,7 @@ def test_5grp_TSO(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=5000) fits.writeto("new_gdq.fits", gdq, overwrite=True) - +@pytest.mark.skip("Used for local testing") def test_5grp_realTSO(): hdul = fits.open("obs2508_cutout_jump.fits") gdq = hdul['groupdq'].data From 3ee10c93003e798394b773a532200e518d078d91 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 12:35:16 -0400 Subject: [PATCH 137/196] remove ds_store --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5873551b20e159e39c741a62b52a32beec4288bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKF=_)r43uIA3~5}t+%Mz@i*a6%4}{q142GQ3UzK;|X`YcpusIIgqzNOCW_M27 z<)%2D%*?l6hi9|3nXTYN`(~ILpVKGyPz=#|#{T$zIvugJ$4T-G!PLv7PX|Jb0Kg^a zFsx&i05%f9UN|O#fq9k!v(#(E@GJ-4Dz6ugiCJzQH{+bT*=s`axE*|pbn~95C Date: Fri, 16 Jun 2023 12:37:33 -0400 Subject: [PATCH 138/196] remove DS_Store --- src/stcal/.DS_Store | Bin 6148 -> 0 bytes src/stcal/jump/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/stcal/.DS_Store delete mode 100644 src/stcal/jump/.DS_Store diff --git a/src/stcal/.DS_Store b/src/stcal/.DS_Store deleted file mode 100644 index c624cc58dfdace2fbdb8472da0dfb8d9d0d7aeab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~JqiLr422WjLa^D=avBfd4F=H@cmYu>Q53{}j_%73f~&QNyg>3zG82}4#m+`V zba_84M0ydKz>PAuFfc{l$x-fdmh0iRKkY7;Q=Stb*@FJ*K=2U&>`->Y+Gh!{SOKib7DNT6(F#VZ`WRw$ zZwE`BtH~COcF`O@G@q=dnMM{&Ba=*Y2R#A8X54gYui6T;v4vKWw@omQT28b?DQlPP9&#l+9 ztDE9{24L&A-7T;Ju%J8Q-NV#;-+f|NE#qi$Mvn*dc*YaDN%iG~bC;~{@PZ?`U;J)2 z^!vxdVb7y+jaMEzFb=2m<@1!40#ZNAml_14MDd9N+>E&XJy ojdF%)#l&dETzD(K`Bqo_nftZjm>6{CgHF_sfa@ZY0)L^v2i7|pWdHyG From 76341c07dfac37dccc035293c7127facdef40496 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 12:39:28 -0400 Subject: [PATCH 139/196] Delete .DS_Store --- src/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 92b154c69b1134b55865669d1f434c936de40b6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~J&pn~427Thk&w2bq)fvBxIu(wPp}tYfpR1Y*gi+!XU7f0)@ZbPmYf$m@%((n zWDLNLKi31W1+byJV(-JmjPV+GJaEA`#_Rnuo^Q9&(^%wQ59qwc^Lj2zL_h>YKmhjAXg1hsgAT2otBW@wh( zgJr2j8{+vWr;Pd@<+{3zbh!?<32LanK-D>F3x2m}TV IBJfrMzvI~xi2wiq From 456bd7d0d6544547d291ba1dd6ddee463ef93da7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 12:41:26 -0400 Subject: [PATCH 140/196] Delete .DS_Store --- tests/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/.DS_Store diff --git a/tests/.DS_Store b/tests/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Fri, 16 Jun 2023 12:51:15 -0400 Subject: [PATCH 141/196] Update CHANGES.rst --- CHANGES.rst | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 93998274..7e706325 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,9 +3,21 @@ Bug Fixes --------- - +Jump +~~~~ +Updated the jump detection to switch to using the numpy sigmaclip routine to +find the actual rms across integrations when there are at least 101 integrations +in the exposure. This still allows cosmic rays and snowballs/showers to be flagged +without being affected by slope variations due to either brigher-fatter/charge-spilling +or errors in the nonlinearity correction. +Also added the counting of the number of cosmic rays and snowballs/showers that +is then placed in the FITS header in the JWST routines. [#382] - - +Ramp Fitting +~~~ +Added another line of code to be included in the section where warnings are turned +off. The large number of warnings can cause a hang in the Jupyter notebook when +running with multiprocessing. Changes to API -------------- From 4088dff76e9088d539c867b5a847492160aa52bf Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 13:37:18 -0400 Subject: [PATCH 142/196] unpacked the use of shape to be named variables --- src/stcal/jump/jump.py | 118 +++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 0d23bce0..82c56e7f 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -336,11 +336,14 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, # Reconstruct gdq, the row_above_gdq, and the row_below_gdq from the # slice result total_primary_crs = 0 + ngrps = gdq.shape[1] + nrows = gdq.shape[2] + ncols = gdq.shape[3] if only_use_ints: - stddev = np.zeros((gdq.shape[1] - 1, gdq.shape[2], gdq.shape[3]), + stddev = np.zeros((ngrps - 1, nrows, ncols), dtype=np.float32) else: - stddev = np.zeros((gdq.shape[2], gdq.shape[3]), + stddev = np.zeros((nrows, ncols), dtype=np.float32) for resultslice in real_result: if len(real_result) == k + 1: # last result @@ -411,7 +414,6 @@ def detect_jumps(frames_per_group, data, gdq, pdq, err, readnoise_2d /= gain_2d # Return the updated data quality arrays - fits.writeto("stddev.fits", stddev, overwrite=True) return gdq, pdq, total_primary_crs, number_extended_events, stddev @@ -468,8 +470,10 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, n_showers_grp = [] total_snowballs = 0 - for integration in range(gdq.shape[0]): - for group in range(1, gdq.shape[1]): + nints = gdq.shape[0] + ngrps = gdq.shape[1] + for integration in range(nints): + for group in range(1, ngrps): current_gdq = 1.0 * gdq[integration, group, :, :] prev_gdq = 1.0 * gdq[integration, group - 1, :, :] diff_gdq = 1.0 * current_gdq - prev_gdq @@ -483,7 +487,8 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, jump_flag, min_jump_area) if sat_required_snowball: low_threshold = edge_size - high_threshold = max(0, gdq.shape[2] - edge_size) + nrows = gdq.shape[2] + high_threshold = max(0, nrows - edge_size) gdq, snowballs = make_snowballs(gdq, integration, group, jump_ellipses, sat_ellipses, @@ -505,7 +510,9 @@ def flag_large_events(gdq, jump_flag, sat_flag, min_sat_area=1, def extend_saturation(cube, grp, sat_ellipses, sat_flag, min_sat_radius_extend, expansion=2, max_extended_radius=200): - image = np.zeros(shape=(cube.shape[1], cube.shape[2], 3), dtype=np.uint8) + ncols = cube.shape[2] + nrows = cube.shape[1] + image = np.zeros(shape=(nrows, ncols, 3), dtype=np.uint8) outcube = cube.copy() for ellipse in sat_ellipses: ceny = ellipse[0][0] @@ -533,7 +540,9 @@ def extend_ellipses(gdq_cube, intg, grp, ellipses, sat_flag, jump_flag, # expanded ellipses of pixels with # the jump flag set. plane = gdq_cube[intg, grp, :, :] - image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) + ncols = plane.shape[1] + nrows = plane.shape[0] + image = np.zeros(shape=(nrows, ncols, 3), dtype=np.uint8) num_ellipses = len(ellipses) for ellipse in ellipses: ceny = ellipse[0][0] @@ -562,7 +571,8 @@ def extend_ellipses(gdq_cube, intg, grp, ellipses, sat_flag, jump_flag, round(axis2 / 2)), alpha, 0, 360, (0, 0, jump_flag), -1) jump_ellipse = image[:, :, 2] - last_grp = min(grp + num_grps_masked, gdq_cube.shape[1]) + ngrps = gdq_cube.shape[1] + last_grp = min(grp + num_grps_masked, ngrps) # This loop will flag the number of groups for flg_grp in range(grp, last_grp): sat_pix = np.bitwise_and(gdq_cube[intg, flg_grp, :, :], sat_flag) @@ -700,12 +710,13 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou all_ellipses = [] first_diffs = np.diff(data, axis=1) first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - if data.shape[0] > minimum_sigclip_groups: + nints = data.shape[0] + if nints > minimum_sigclip_groups: mean, median, stddev = stats.sigma_clipped_stats(first_diffs_masked, sigma=5, axis=0) - for intg in range(data.shape[0]): + for intg in range(nints): # calculate sigma for each pixel - if data.shape[0] <= minimum_sigclip_groups: + if nints <= minimum_sigclip_groups: median_diffs = np.nanmedian(first_diffs_masked[intg], axis=0) sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) # The difference from the median difference for each group @@ -715,8 +726,9 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou # The convolution kernal creation ring_2D_kernel = Ring2DKernel(inner, outer) - for grp in range(1, data.shape[1]): - if data.shape[0] > minimum_sigclip_groups: + ngrps = data.shape[1] + for grp in range(1, ngrps): + if nints > minimum_sigclip_groups: median_diffs = median[grp-1] sigma = stddev[grp-1] # The difference from the median difference for each group @@ -734,8 +746,10 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou masked_ratio[saty, satx] = np.nan masked_smoothed_ratio = convolve(masked_ratio, ring_2D_kernel) - extended_emission = np.zeros(shape=(ratio.shape[1], - ratio.shape[2]), dtype=np.uint8) + nrows = ratio.shape[1] + ncols = ratio.shape[2] + extended_emission = np.zeros(shape=(nrows, + ncols), dtype=np.uint8) exty, extx = np.where(masked_smoothed_ratio > snr_threshold) extended_emission[exty, extx] = 1 # find the contours of the extended emission @@ -748,43 +762,43 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou # get the minimum enclosing rectangle which is the same as the # minimum enclosing ellipse ellipses = [cv.minAreaRect(con) for con in bigcontours] - if grp==179 and intg == 0: - - expand_by_ratio = True - expansion = 1.0 - plane = gdq[intg, grp, :, :] - image = np.zeros(shape=(plane.shape[0], plane.shape[1], 3), dtype=np.uint8) - image2 = np.zeros_like(image) - cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), -1) - big_contour_image = image2[:, :, 2] - num_ellipses = len(ellipses) - for ellipse in ellipses: - ceny = ellipse[0][0] - cenx = ellipse[0][1] - # Expand the ellipse by the expansion factor. The number of pixels - # added to both axes is - # the number of pixels added to the minor axis. This prevents very - # large flagged ellipses - # with high axis ratio ellipses. The major and minor axis are not - # always the same index. - # Therefore, we have to test to find which is actually the minor axis. - if expand_by_ratio: - if ellipse[1][1] < ellipse[1][0]: - axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] - axis2 = ellipse[1][1] * expansion - else: - axis1 = ellipse[1][0] * expansion - axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + + + expand_by_ratio = True + expansion = 1.0 + plane = gdq[intg, grp, :, :] + nrows = plane.shape[0] + ncols = plane.shape[1] + image = np.zeros(shape=(nrows, ncols, 3), dtype=np.uint8) + image2 = np.zeros_like(image) + cv.drawContours(image2, bigcontours, -1, (0, 0, jump_flag), -1) + for ellipse in ellipses: + ceny = ellipse[0][0] + cenx = ellipse[0][1] + # Expand the ellipse by the expansion factor. The number of pixels + # added to both axes is + # the number of pixels added to the minor axis. This prevents very + # large flagged ellipses + # with high axis ratio ellipses. The major and minor axis are not + # always the same index. + # Therefore, we have to test to find which is actually the minor axis. + if expand_by_ratio: + if ellipse[1][1] < ellipse[1][0]: + axis1 = ellipse[1][0] + (expansion - 1.0) * ellipse[1][1] + axis2 = ellipse[1][1] * expansion else: - axis1 = ellipse[1][0] + expansion - axis2 = ellipse[1][1] + expansion - axis1 = min(axis1, max_extended_radius) - axis2 = min(axis2, max_extended_radius) - alpha = ellipse[2] - image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), - round(axis2 / 2)), alpha, 0, 360, - (0, 0, jump_flag), -1) - jump_ellipse = image[:, :, 2] + axis1 = ellipse[1][0] * expansion + axis2 = ellipse[1][1] + (expansion - 1.0) * ellipse[1][0] + else: + axis1 = ellipse[1][0] + expansion + axis2 = ellipse[1][1] + expansion + axis1 = min(axis1, max_extended_radius) + axis2 = min(axis2, max_extended_radius) + alpha = ellipse[2] + image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), + round(axis2 / 2)), alpha, 0, 360, + (0, 0, jump_flag), -1) + jump_ellipse = image[:, :, 2] if len(ellipses) > 0: # add all the showers for this integration to the list all_ellipses.append([intg, grp, ellipses]) From 4c199953e8b1d0408c330bb8e55a574493a39b67 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 16 Jun 2023 15:45:52 -0400 Subject: [PATCH 143/196] replacing shape with meaningful variable names --- .gitignore | 3 +-- src/stcal/jump/twopoint_difference.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 60caacb9..62371214 100644 --- a/.gitignore +++ b/.gitignore @@ -143,5 +143,4 @@ src/stcal/_version.py # auto-generated API docs docs/source/api -.DS_Store -.DS_Store +.DS_Store \ No newline at end of file diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index a0d9d5fe..ec1687b1 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -103,7 +103,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq = group_dq.copy() else: gdq = group_dq - # Get data characteristics + # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape ndiffs = ngroups - 1 # get readnoise, squared @@ -121,25 +121,25 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, dat = dataa num_flagged_grps = 0 # determine the number of groups with all pixels set to DO_NOT_USE - for integ in range(dat.shape[0]): + nints = nints + ngrps = dat.shape[1] + for integ in range(nints): for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 - if only_use_ints and dat.shape[0]: - total_groups = dat.shape[0] + if only_use_ints and nints: + total_groups = nints else: - total_groups = dat.shape[0] * dat.shape[1] - num_flagged_grps - if (dat.shape[1] < minimum_groups and only_use_ints and dat.shape[0] < minimum_sigclip_groups) or \ - (not only_use_ints and dat.shape[0] * dat.shape[1] < minimum_sigclip_groups and - dat.shape[1] < minimum_groups): + total_groups = nints * ngrps - num_flagged_grps + if (ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or \ + (not only_use_ints and nints * ngrps < minimum_sigclip_groups and + ngrps < minimum_groups): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") log.info("Data shape {}".format(str(dat.shape))) dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), dtype=np.float32) return gdq, row_below_gdq, row_above_gdq, 0, dummy else: - - # set 'saturated' or 'do not use' pixels to nan in data # set 'saturated' or 'do not use' pixels to nan in data dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan dat[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan @@ -190,8 +190,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) # if grp is all jump set to do not use - for integ in range(dat.shape[0]): - for grp in range(dat.shape[1]): + for integ in range(nints): + for grp in range(ngrps): if np.all(np.bitwise_or(np.bitwise_and(gdq[integ, grp, :, :], jump_flag), np.bitwise_and(gdq[integ, grp, :, :], dnu_flag))): jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) From 13819d2f7e5df945cbfeb51e8edd965d387af4df Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 20 Jun 2023 11:54:06 -0400 Subject: [PATCH 144/196] fix style issues --- src/stcal/jump/jump.py | 2 -- tests/test_ramp_fitting.py | 1 - tests/test_twopoint_difference.py | 6 ++---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 82c56e7f..e2b4f856 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -3,7 +3,6 @@ import time import numpy as np import cv2 as cv -from astropy.io import fits import astropy.stats as stats from astropy.convolution import Ring2DKernel @@ -798,7 +797,6 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou image = cv.ellipse(image, (round(ceny), round(cenx)), (round(axis1 / 2), round(axis2 / 2)), alpha, 0, 360, (0, 0, jump_flag), -1) - jump_ellipse = image[:, :, 2] if len(ellipses) > 0: # add all the showers for this integration to the list all_ellipses.append([intg, grp, ellipses]) diff --git a/tests/test_ramp_fitting.py b/tests/test_ramp_fitting.py index 52148efa..b9902858 100644 --- a/tests/test_ramp_fitting.py +++ b/tests/test_ramp_fitting.py @@ -1,5 +1,4 @@ import numpy as np -from astropy.io import fits from stcal.ramp_fitting.ramp_fit import ramp_fit_data from stcal.ramp_fitting.ramp_fit_class import RampData diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e38c075c..d4634a1f 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1028,7 +1028,6 @@ def test_sigma_clip(): indata = data[:53, :, :, :] ingdq = gdq[:53, :, :, :] read_noise = np.ones(shape=(indata.shape[2], indata.shape[3]), dtype=np.float32) * 5.9 * 4.0 - gain = np.ones_like(indata) * 4.0 hdul.close() gdq, row_below_gdq, row_above_gdq, total_primary_crs, stddev = find_crs(indata, ingdq, read_noise, 3, 4, 5, 1, @@ -1046,8 +1045,6 @@ def test_first_grp_flag_issue(): nrows = 2 ncols = 2 ngroups = 10 - rej_threshold = 3 - nframes = 1 readnoise = 2 data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) data = np.random.normal(0, readnoise, size=(nints, ngroups, nrows, ncols)) @@ -1128,4 +1125,5 @@ def test_1059(): after_jump_flag_e1=0.0, after_jump_flag_n1=0, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) - fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) \ No newline at end of file + fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) + \ No newline at end of file From 3a8c2acdf73876a83cdc5cf6ade9fc388c1f74c7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 20 Jun 2023 11:58:14 -0400 Subject: [PATCH 145/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index d4634a1f..cab8162f 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1126,4 +1126,3 @@ def test_1059(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) - \ No newline at end of file From 9b4c8f5fe28cf3c6e68a16f6a5df8eab496d07dd Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Wed, 21 Jun 2023 09:19:19 -0400 Subject: [PATCH 146/196] Delete setup.cfg --- setup.cfg | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e69de29b..00000000 From c0b33bf1097be66993893ac9bbf88a7254b633ea Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 22 Jun 2023 19:54:37 -0400 Subject: [PATCH 147/196] Fix missing loop in flag 4 neighbors I added a loop over integrations that was missing due to the post CR flagging being done once for all ints. --- src/stcal/jump/twopoint_difference.py | 23 ++++++++++++----------- tests/test_twopoint_difference.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index ec1687b1..4eb8485e 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -161,7 +161,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # compute 'ratio' for each group. this is the value that will be # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, :, :] + e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] # if nints > 1: ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] @@ -361,16 +361,17 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - for j in range(len(cr_group)): - group = cr_group[j] - row = cr_row[j] - col = cr_col[j] - if e_jump[group - 1, row, col] >= cthres[row, col]: - for kk in range(group, min(group + cgroup + 1, ngroups)): - if (gdq[integ, kk, row, col] & sat_flag) == 0: - if (gdq[integ, kk, row, col] & dnu_flag) == 0: - gdq[integ, kk, row, col] = \ - np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + for intg in range(nints): + for j in range(len(cr_group)): + group = cr_group[j] + row = cr_row[j] + col = cr_col[j] + if e_jump_4d[intg, group - 1, row, col] >= cthres[row, col]: + for kk in range(group, min(group + cgroup + 1, ngroups)): + if (gdq[intg, kk, row, col] & sat_flag) == 0: + if (gdq[intg, kk, row, col] & dnu_flag) == 0: + gdq[intg, kk, row, col] = \ + np.bitwise_or(gdq[integ, kk, row, col], jump_flag) if 'stddev' in locals(): return gdq, row_below_gdq, row_above_gdq, num_primary_crs, stddev else: diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index cab8162f..303c425d 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1126,3 +1126,18 @@ def test_1059(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) + +@pytest.mark.skip("Used for local testing") +def test_1952(): + hdul = fits.open("data/obs1952_sc_shower_00_dark_current.fits") + gdq = hdul['groupdq'].data + data = hdul['sci'].data * 5.5 + readnoise = 5.5 * 6 + read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) + gain = np.full((gdq.shape[2], gdq.shape[3]), 5.5, dtype=np.float32) + gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ + find_crs(data, gdq, read_noise, 5, 4, 5, 1, True, 1000, 10, DQFLAGS, + after_jump_flag_e1=100.0 * gain, after_jump_flag_n1=20/2.77, + after_jump_flag_e2=3000.0 * gain, after_jump_flag_n2=1000/2.77, + copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=150) + fits.writeto("new_obs1952_gdq.fits", gdq, overwrite=True) From a866d4c7c1e187856842f43b22712ae1c436da02 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 23 Jun 2023 02:07:17 -0400 Subject: [PATCH 148/196] Update twopoint_difference.py Remove loop over integrations and use where to get integration. --- src/stcal/jump/twopoint_difference.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 4eb8485e..847f969b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -358,20 +358,21 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] flag_groups = [after_jump_flag_n1, after_jump_flag_n2] - cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq[integ], jump_flag)) + for cthres, cgroup in zip(flag_e_threshold, flag_groups): if cgroup > 0: - for intg in range(nints): - for j in range(len(cr_group)): - group = cr_group[j] - row = cr_row[j] - col = cr_col[j] - if e_jump_4d[intg, group - 1, row, col] >= cthres[row, col]: - for kk in range(group, min(group + cgroup + 1, ngroups)): - if (gdq[intg, kk, row, col] & sat_flag) == 0: - if (gdq[intg, kk, row, col] & dnu_flag) == 0: - gdq[intg, kk, row, col] = \ - np.bitwise_or(gdq[integ, kk, row, col], jump_flag) + cr_intg, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) + for j in range(len(cr_group)): + intg = cr_intg[j] + group = cr_group[j] + row = cr_row[j] + col = cr_col[j] + if e_jump_4d[intg, group - 1, row, col] >= cthres[row, col]: + for kk in range(group, min(group + cgroup + 1, ngroups)): + if (gdq[intg, kk, row, col] & sat_flag) == 0: + if (gdq[intg, kk, row, col] & dnu_flag) == 0: + gdq[intg, kk, row, col] = \ + np.bitwise_or(gdq[integ, kk, row, col], jump_flag) if 'stddev' in locals(): return gdq, row_below_gdq, row_above_gdq, num_primary_crs, stddev else: From 9ff99936787c9a7aa355aedd992484ad14dd744f Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Fri, 23 Jun 2023 11:35:13 -0400 Subject: [PATCH 149/196] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 24d8f57d..eaa575aa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,7 +11,7 @@ in the exposure. This still allows cosmic rays and snowballs/showers to be flagg without being affected by slope variations due to either brigher-fatter/charge-spilling or errors in the nonlinearity correction. Also added the counting of the number of cosmic rays and snowballs/showers that -is then placed in the FITS header in the JWST routines. [#382] +is then placed in the FITS header in the JWST routines. [#174] - Ramp Fitting ~~~ From e2a2aa5dbd7354df34f066c4e84eb39681561656 Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Fri, 23 Jun 2023 11:35:24 -0400 Subject: [PATCH 150/196] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index eaa575aa..a0d14c31 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,7 @@ Ramp Fitting ~~~ Added another line of code to be included in the section where warnings are turned off. The large number of warnings can cause a hang in the Jupyter notebook when -running with multiprocessing. +running with multiprocessing. [#174] Changes to API -------------- From 4ed2e90737e8e3f4e51350dd55de85bbe6b9ac66 Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Fri, 23 Jun 2023 11:35:33 -0400 Subject: [PATCH 151/196] Update src/stcal/jump/jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index e2b4f856..3b162946 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -718,7 +718,7 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou if nints <= minimum_sigclip_groups: median_diffs = np.nanmedian(first_diffs_masked[intg], axis=0) sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - # The difference from the median difference for each group + # The difference from the median difference for each group e_jump = first_diffs_masked[intg] - median_diffs[np.newaxis, :, :] # SNR ratio of each diff. ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] From f6a1ac59e6b962367342218e99fbfb910949d9bf Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Fri, 23 Jun 2023 11:35:46 -0400 Subject: [PATCH 152/196] Update src/stcal/jump/jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 3b162946..c6ad57da 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -720,7 +720,7 @@ def find_faint_extended(indata, gdq, readnoise_2d, nframes, minimum_sigclip_grou sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) # The difference from the median difference for each group e_jump = first_diffs_masked[intg] - median_diffs[np.newaxis, :, :] - # SNR ratio of each diff. + # SNR ratio of each diff. ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] # The convolution kernal creation From 259a2afaed26c7fea266ce9e3a4e85bb4ac95afd Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Jun 2023 20:24:35 -0400 Subject: [PATCH 153/196] Update twopoint_difference.py Resolve comments on PR. --- src/stcal/jump/twopoint_difference.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 847f969b..8420b307 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -85,6 +85,19 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, Flag for making internal copies of the arrays so the input isn't modified, defaults to True. + minimum_groups : integer + The minimum number of groups to perform jump detection. + + minimum_sigclip_groups : integer + The minimum number of groups required for the sigma clip routine to be + used for jump detection rather than using the expected noise based on + the read noise and gain files. + + only_use_ints : boolean + If True the sigma clip process will only apply for groups between + integrations. This means that a group will only be compared against the + same group in other integrations. If False all groups across all integrations + will be used to detect outliers. Returns ------- gdq : int, 4D array @@ -99,9 +112,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, """ # copy data and group DQ array if copy_arrs: - dataa = dataa.copy() + dat = dataa.copy() gdq = group_dq.copy() else: + dat = dataa gdq = group_dq # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape @@ -118,10 +132,8 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jump_flag = dqflags["JUMP_DET"] # get data, gdq - dat = dataa num_flagged_grps = 0 # determine the number of groups with all pixels set to DO_NOT_USE - nints = nints ngrps = dat.shape[1] for integ in range(nints): for grp in range(dat.shape[1]): @@ -162,7 +174,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] -# if nints > 1: ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] if (only_use_ints and nints >= minimum_sigclip_groups) or \ From 8d713156df9a6b0a1916200ef38457e24026630d Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 10 Jul 2023 11:58:11 -0400 Subject: [PATCH 154/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 8420b307..af1e9917 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -172,7 +172,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # compute 'ratio' for each group. this is the value that will be # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. + # first_diffs from first_diffs, take the absolute value and divide by sigma. e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] From d2fa8d72221e91861beb14e6663c25284ec3188c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 13 Dec 2023 15:35:56 -0500 Subject: [PATCH 155/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index af1e9917..99fb1d8b 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -209,23 +209,19 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[integ, grp, jumpy, jumpx] = 0 warnings.resetwarnings() else: - for integ in range(nints): - # get data, gdq for this integration - dat = dataa[integ] - gdq_integ = gdq[integ] # set 'saturated' or 'do not use' pixels to nan in data - dat[np.where(np.bitwise_and(gdq_integ, sat_flag))] = np.nan - dat[np.where(np.bitwise_and(gdq_integ, dnu_flag))] = np.nan + dataa[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan + dataa[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dat, axis=0) + first_diffs = np.diff(dataa, axis=1) # calc. the median of first_diffs for each pixel along the group axis - median_diffs = calc_med_first_diffs(first_diffs) - +# median_diffs = calc_med_first_diffs(first_diffs) + median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) # reset sigma so pxels with 0 readnoise are not flagged as jumps @@ -234,16 +230,21 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # compute 'ratio' for each group. this is the value that will be # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] + e_jump = first_diffs - median_diffs[np.newaxis, np.newaxis, :, :] + + ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] # create a 2d array containing the value of the largest 'ratio' for each group warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) max_ratio = np.nanmax(ratio, axis=0) warnings.resetwarnings() + + all_crs_row, all_crs_col = np.where(ratio > normal_rej_thresh) + num_unusable_groups = np.sum(np.isnan(first_diffs), axis=(0, 1)) + if ndiffs - num_unusable_groups < 4: # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups - num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) + row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, max_ratio > normal_rej_thresh)) row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, @@ -255,7 +256,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) - # iterate over all groups of the pix w/ an inital CR to look for subsequent CRs + # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs # flag and clip the first CR found. recompute median/sigma/ratio # and repeat the above steps of comparing the max 'ratio' for each pixel # to the threshold to determine if another CR can be flagged and clipped. From 95059c6f20007d079b04f925d8ff94231a05390d Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 3 Jan 2024 11:50:35 -0500 Subject: [PATCH 156/196] updates --- src/stcal/jump/twopoint_difference.py | 169 ++++++++++++++------------ tests/test_twopoint_difference.py | 48 ++++---- 2 files changed, 112 insertions(+), 105 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 99fb1d8b..3dc8171c 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -119,7 +119,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq = group_dq # Get data characteristics nints, ngroups, nrows, ncols = dataa.shape - ndiffs = ngroups - 1 + ndiffs = (ngroups - 1) * nints # get readnoise, squared read_noise_2 = read_noise**2 # create arrays for output @@ -139,13 +139,15 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, for grp in range(dat.shape[1]): if np.all(np.bitwise_and(gdq[integ, grp, :, :], dnu_flag)): num_flagged_grps += 1 - if only_use_ints and nints: - total_groups = nints + if only_use_ints: + total_sigclip_groups = nints else: - total_groups = nints * ngrps - num_flagged_grps + total_sigclip_groups = nints * (ngrps - num_flagged_grps) + total_groups = nints * (ngrps - num_flagged_grps) + total_diffs = nints * (ngrps - 1 - num_flagged_grps) if (ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or \ (not only_use_ints and nints * ngrps < minimum_sigclip_groups and - ngrps < minimum_groups): + total_groups < minimum_groups): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") log.info("Data shape {}".format(str(dat.shape))) dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), @@ -176,6 +178,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] + # Test to see if there are enough group to use sigma clipping if (only_use_ints and nints >= minimum_sigclip_groups) or \ (not only_use_ints and total_groups >= minimum_sigclip_groups): log.info(" Jump Step using sigma clip {} greater than {}, rejection threshold {}".format( @@ -208,23 +211,22 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 warnings.resetwarnings() - else: - # get data, gdq for this integration + else: # There are not enough groups for sigma clipping # set 'saturated' or 'do not use' pixels to nan in data - dataa[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan - dataa[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan + dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan + dat[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dataa, axis=1) + first_diffs = np.diff(dat, axis=1) # calc. the median of first_diffs for each pixel along the group axis # median_diffs = calc_med_first_diffs(first_diffs) median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - # reset sigma so pxels with 0 readnoise are not flagged as jumps + # reset sigma so pixels with 0 read noise are not flagged as jumps sigma[np.where(sigma == 0.)] = np.nan # compute 'ratio' for each group. this is the value that will be @@ -239,76 +241,83 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, max_ratio = np.nanmax(ratio, axis=0) warnings.resetwarnings() - all_crs_row, all_crs_col = np.where(ratio > normal_rej_thresh) num_unusable_groups = np.sum(np.isnan(first_diffs), axis=(0, 1)) - if ndiffs - num_unusable_groups < 4: - # now see if the largest ratio of all groups for each pixel exceeds the threshold. - # there are different threshold for 4+, 3, and 2 usable groups - - row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, - max_ratio > normal_rej_thresh)) - row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, - max_ratio > three_diff_rej_thresh)) - row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, - max_ratio > two_diff_rej_thresh)) - - # get the rows, col pairs for all pixels with at least one CR - all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) - all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) - - # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs - # flag and clip the first CR found. recompute median/sigma/ratio - # and repeat the above steps of comparing the max 'ratio' for each pixel - # to the threshold to determine if another CR can be flagged and clipped. - # repeat this process until no more CRs are found. - for j in range(len(all_crs_row)): - # get arrays of abs(diffs), ratio, readnoise for this pixel - pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] - pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] - pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] - - # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR - pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) - - # set the largest ratio as a CR - pix_cr_mask[np.nanargmax(pix_ratio)] = 0 - new_CR_found = True - - # loop and check for more CRs, setting the mask as you go and - # clipping the group with the CR. stop when no more CRs are found - # or there is only one two diffs left (which means there is - # actually one left, since the next CR will be masked after - # checking that condition) - while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): - - new_CR_found = False - - # set CRs to nans in first diffs to clip them - pix_first_diffs[~pix_cr_mask] = np.nan - - # recalculate median, sigma, and ratio - new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) - - new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) - new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma - - # check if largest ratio exceeds threhold appropriate for num remaining groups - - # select appropriate thresh. based on number of remaining groups - rej_thresh = normal_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: - rej_thresh = three_diff_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: - rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio - if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: - new_CR_found = True - pix_cr_mask[new_pix_max_ratio_idx] = 0 - unusable_diffs = np.sum(np.isnan(pix_first_diffs)) - # Found all CRs for this pix - set flags in input DQ array - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ - np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], - dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) + if total_diffs > 2: # enough diffs to calculate the outliers + pix_cr_mask = np.zeros(ratio.shape, dtype=bool) + pix_flagged = np.zeros(dat.shape, dtype=bool) + jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) + pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 + pix_flagged[:, 1:, :, :] = pix_cr_mask + gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) + test = 1 + else: + # now see if the largest ratio of all groups for each pixel exceeds the threshold. + # there are different threshold for 4+, 3, and 2 usable groups + + row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, + max_ratio > normal_rej_thresh)) + row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, + max_ratio > three_diff_rej_thresh)) + row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, + max_ratio > two_diff_rej_thresh)) + + # get the rows, col pairs for all pixels with at least one CR + all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) + all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) + + # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs + # flag and clip the first CR found. recompute median/sigma/ratio + # and repeat the above steps of comparing the max 'ratio' for each pixel + # to the threshold to determine if another CR can be flagged and clipped. + # repeat this process until no more CRs are found. + for j in range(len(all_crs_row)): + # get arrays of abs(diffs), ratio, readnoise for this pixel + pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] + pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] + pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] + + # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR + pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) + + # set the largest ratio as a CR + pix_cr_mask[np.nanargmax(pix_ratio)] = 0 + new_CR_found = True + + # loop and check for more CRs, setting the mask as you go and + # clipping the group with the CR. stop when no more CRs are found + # or there is only one two diffs left (which means there is + # actually one left, since the next CR will be masked after + # checking that condition) + while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): + + new_CR_found = False + + # set CRs to nans in first diffs to clip them + pix_first_diffs[~pix_cr_mask] = np.nan + + # recalculate median, sigma, and ratio + new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) + + new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) + new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma + + # check if largest ratio exceeds threhold appropriate for num remaining groups + + # select appropriate thresh. based on number of remaining groups + rej_thresh = normal_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: + rej_thresh = three_diff_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: + rej_thresh = two_diff_rej_thresh + new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: + new_CR_found = True + pix_cr_mask[new_pix_max_ratio_idx] = 0 + unusable_diffs = np.sum(np.isnan(pix_first_diffs)) + # Found all CRs for this pix - set flags in input DQ array + gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ + np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], + dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 303c425d..e19b7de3 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -11,10 +11,7 @@ @pytest.fixture(scope='function') def setup_cube(): - def _cube(ngroups, readnoise=10): - nints = 1 - nrows = 204 - ncols = 204 + def _cube(ngroups, nints=1, nrows=204, ncols=204, readnoise=10): rej_threshold = 3 nframes = 1 data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) @@ -116,20 +113,21 @@ def test_4grps_cr2_noflux(setup_cube): assert(1 == np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group -def test_5grps_cr2_nframe2(setup_cube): - ngroups = 5 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) +def test_6grps_cr2_nframe2(setup_cube): + ngroups = 6 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nrows=2, ncols=2) nframes = 2 - data[0, 0, 100, 100] = 10.0 - data[0, 1, 100, 100] = 500 - data[0, 2, 100, 100] = 1002 - data[0, 3, 100, 100] = 1001 - data[0, 4, 100, 100] = 1005 + data[0, 0, 1, 1] = 10.0 + data[0, 1, 1, 1] = 500 + data[0, 2, 1, 1] = 1002 + data[0, 3, 1, 1] = 1001 + data[0, 4, 1, 1] = 1005 + data[0, 5, 1, 1] = 1015 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found - assert(np.array_equal([0, 4, 4, 0, 0], out_gdq[0, :, 100, 100])) + assert(np.array_equal([0, 4, 4, 0, 0, 0, 0], out_gdq[0, :, 1, 1])) @pytest.mark.xfail @@ -665,22 +663,22 @@ def test_10grps_satat8_crsat3and6(setup_cube): def test_median_with_saturation(setup_cube): ngroups = 10 # crmag = 1000 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=5 * np.sqrt(2)) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nrows=2, ncols=2, readnoise=5 * np.sqrt(2)) nframes = 1 - data[0, 0, 100, 100] = 0 - data[0, 1, 100, 100] = 4500 - data[0, 2, 100, 100] = 9100 - data[0, 3, 100, 100] = 13800 - data[0, 4, 100, 100] = 18600 - data[0, 5, 100, 100] = 40000 # CR - data[0, 6, 100, 100] = 44850 - data[0, 7, 100, 100] = 49900 - data[0, 8:10, 100, 100] = 60000 - gdq[0, 7:10, 100, 100] = DQFLAGS['SATURATED'] + data[0, 0, 1, 1] = 0 + data[0, 1, 1, 1] = 4500 + data[0, 2, 1, 1] = 9100 + data[0, 3, 1, 1] = 13800 + data[0, 4, 1, 1] = 18600 + data[0, 5, 1, 1] = 40000 # CR + data[0, 6, 1, 1] = 44850 + data[0, 7, 1, 1] = 49900 + data[0, 8:10, 1, 1] = 60000 + gdq[0, 7:10, 1, 1] = DQFLAGS['SATURATED'] out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) - assert (np.array_equal([0, 0, 0, 0, 0, 4, 0, 2, 2, 2], out_gdq[0, :, 100, 100])) + assert (np.array_equal([0, 0, 0, 0, 0, 4, 0, 2, 2, 2], out_gdq[0, :, 1, 1])) def test_median_with_saturation_even_num_sat_frames(setup_cube): From 55f8cfd3f7daabf6f8ad50ec59098962794a1a1c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 9 Feb 2024 13:18:40 -0500 Subject: [PATCH 157/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 3dc8171c..12d1f78e 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -249,7 +249,6 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 pix_flagged[:, 1:, :, :] = pix_cr_mask gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) - test = 1 else: # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups From 4d09518afc718e1e42e9dc72cc1301b154f12086 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 9 Feb 2024 14:38:17 -0500 Subject: [PATCH 158/196] updates --- src/stcal/jump/twopoint_difference.py | 18 +++++++++--------- tests/test_twopoint_difference.py | 7 ++++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 12d1f78e..c57c504f 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -237,9 +237,9 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] # create a 2d array containing the value of the largest 'ratio' for each group - warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) - max_ratio = np.nanmax(ratio, axis=0) - warnings.resetwarnings() +# warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) +# max_ratio = np.nanmax(ratio, axis=0) +# warnings.resetwarnings() num_unusable_groups = np.sum(np.isnan(first_diffs), axis=(0, 1)) if total_diffs > 2: # enough diffs to calculate the outliers @@ -253,12 +253,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups - row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, - max_ratio > normal_rej_thresh)) - row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, - max_ratio > three_diff_rej_thresh)) - row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, - max_ratio > two_diff_rej_thresh)) + int4cr, row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, + ratio > normal_rej_thresh)) + int3cr, row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, + ratio > three_diff_rej_thresh)) + int2cr, row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, + ratio > two_diff_rej_thresh)) # get the rows, col pairs for all pixels with at least one CR all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e19b7de3..81c0ecf1 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -666,9 +666,9 @@ def test_median_with_saturation(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nrows=2, ncols=2, readnoise=5 * np.sqrt(2)) nframes = 1 data[0, 0, 1, 1] = 0 - data[0, 1, 1, 1] = 4500 - data[0, 2, 1, 1] = 9100 - data[0, 3, 1, 1] = 13800 + data[0, 1, 1, 1] = 4700 + data[0, 2, 1, 1] = 9400 + data[0, 3, 1, 1] = 14300 data[0, 4, 1, 1] = 18600 data[0, 5, 1, 1] = 40000 # CR data[0, 6, 1, 1] = 44850 @@ -678,6 +678,7 @@ def test_median_with_saturation(setup_cube): out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) + gdq_value = out_gdq[0, :, 1, 1] assert (np.array_equal([0, 0, 0, 0, 0, 4, 0, 2, 2, 2], out_gdq[0, :, 1, 1])) From 2393563f558bc98935ebf3bd5caa949ec58861bc Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 9 Feb 2024 14:59:02 -0500 Subject: [PATCH 159/196] update tests --- src/stcal/jump/twopoint_difference.py | 2 +- tests/test_twopoint_difference.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index c57c504f..58195cb0 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -211,7 +211,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) gdq[integ, grp, jumpy, jumpx] = 0 warnings.resetwarnings() - else: # There are not enough groups for sigma clipping + else: # There are not enough groups for sigma clipping # set 'saturated' or 'do not use' pixels to nan in data dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 81c0ecf1..5689b5b0 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -566,22 +566,22 @@ def test_10grps_nocr_2pixels_sigma0(setup_cube): def test_5grps_satat4_crat3(setup_cube): ngroups = 5 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=5 * np.sqrt(2)) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nrows=2, ncols=2, readnoise=5 * np.sqrt(2)) nframes = 1 - data[0, 0, 100, 100] = 10000 - data[0, 1, 100, 100] = 30000 - data[0, 2, 100, 100] = 60000 - data[0, 3, 100, 100] = 61000 - data[0, 4, 100, 100] = 61000 - gdq[0, 3, 100, 100] = DQFLAGS['SATURATED'] - gdq[0, 4, 100, 100] = DQFLAGS['SATURATED'] + data[0, 0, 1, 1] = 10000 + data[0, 1, 1, 1] = 35000 + data[0, 2, 1, 1] = 60000 + data[0, 3, 1, 1] = 61000 + data[0, 4, 1, 1] = 61000 + gdq[0, 3, 1, 1] = DQFLAGS['SATURATED'] + gdq[0, 4, 1, 1] = DQFLAGS['SATURATED'] out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found assert np.array_equal( [0, 0, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], - out_gdq[0, :, 100, 100]) + out_gdq[0, :, 1, 1]) def test_6grps_satat6_crat1(setup_cube): @@ -669,7 +669,7 @@ def test_median_with_saturation(setup_cube): data[0, 1, 1, 1] = 4700 data[0, 2, 1, 1] = 9400 data[0, 3, 1, 1] = 14300 - data[0, 4, 1, 1] = 18600 + data[0, 4, 1, 1] = 19100 data[0, 5, 1, 1] = 40000 # CR data[0, 6, 1, 1] = 44850 data[0, 7, 1, 1] = 49900 From 3be5bd3d26b59356d4a0ef18f4d6f2fee70b315e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 9 Feb 2024 16:07:23 -0500 Subject: [PATCH 160/196] updates --- src/stcal/jump/twopoint_difference.py | 11 ++++++++++- tests/test_twopoint_difference.py | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 58195cb0..dc238b66 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -241,7 +241,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # max_ratio = np.nanmax(ratio, axis=0) # warnings.resetwarnings() - num_unusable_groups = np.sum(np.isnan(first_diffs), axis=(0, 1)) + num_unusable_diffs = np.sum(np.isnan(first_diffs), axis=(0, 1)) if total_diffs > 2: # enough diffs to calculate the outliers pix_cr_mask = np.zeros(ratio.shape, dtype=bool) pix_flagged = np.zeros(dat.shape, dtype=bool) @@ -249,6 +249,14 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 pix_flagged[:, 1:, :, :] = pix_cr_mask gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) + # deal with pixels with only two good diffs + good_diffs = np.ones(shape=(nrows, ncols)) * total_diffs - num_unusable_diffs + row2gd, col2gd = np.where(good_diffs == 2) + for idx in row2gd.shape: + if np.max(ratio[:, :, row2gd[idx], col2gd[idx]]) > two_diff_rej_thresh: + if ratio[] + gdq[int2gd[idx], np.argmax(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] + gdq[int2gd[idx], np.argmin(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = 0 else: # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups @@ -257,6 +265,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, ratio > normal_rej_thresh)) int3cr, row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, ratio > three_diff_rej_thresh)) + int2cr, row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, ratio > two_diff_rej_thresh)) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 5689b5b0..db95825e 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -569,7 +569,7 @@ def test_5grps_satat4_crat3(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nrows=2, ncols=2, readnoise=5 * np.sqrt(2)) nframes = 1 data[0, 0, 1, 1] = 10000 - data[0, 1, 1, 1] = 35000 + data[0, 1, 1, 1] = 20000 data[0, 2, 1, 1] = 60000 data[0, 3, 1, 1] = 61000 data[0, 4, 1, 1] = 61000 @@ -579,6 +579,7 @@ def test_5grps_satat4_crat3(setup_cube): rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found + result = out_gdq[0, :, 1, 1] assert np.array_equal( [0, 0, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], out_gdq[0, :, 1, 1]) From cd6c31ce93f0285eb182e6d855db1f50721895c0 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Feb 2024 10:13:45 -0500 Subject: [PATCH 161/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index dc238b66..20bc6938 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -252,11 +252,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # deal with pixels with only two good diffs good_diffs = np.ones(shape=(nrows, ncols)) * total_diffs - num_unusable_diffs row2gd, col2gd = np.where(good_diffs == 2) +# gdq[:, :, row2gd, col2gd] = dqflags["JUMP_DET"] for idx in row2gd.shape: if np.max(ratio[:, :, row2gd[idx], col2gd[idx]]) > two_diff_rej_thresh: - if ratio[] - gdq[int2gd[idx], np.argmax(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] - gdq[int2gd[idx], np.argmin(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = 0 + gdq[:, :, row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] +# gdq[int2gd[idx], np.argmax(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] +# gdq[int2gd[idx], np.argmin(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = 0 else: # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups From d00246b715c4c3dc83a8b69b33eeb3dc55aa97b2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 12 Feb 2024 14:12:27 -0500 Subject: [PATCH 162/196] updates --- src/stcal/jump/twopoint_difference.py | 116 +++++++++++--------------- tests/test_twopoint_difference.py | 35 +++++++- 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 20bc6938..5d2829d1 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -240,7 +240,10 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) # max_ratio = np.nanmax(ratio, axis=0) # warnings.resetwarnings() - + e100 = e_jump[:, :, 100, 100] + rall100 = ratio_all[:, :, 100, 100] + r100 = ratio[:, :, 100, 100] + f100 = first_diffs[:, :, 100, 100] num_unusable_diffs = np.sum(np.isnan(first_diffs), axis=(0, 1)) if total_diffs > 2: # enough diffs to calculate the outliers pix_cr_mask = np.zeros(ratio.shape, dtype=bool) @@ -253,80 +256,55 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, good_diffs = np.ones(shape=(nrows, ncols)) * total_diffs - num_unusable_diffs row2gd, col2gd = np.where(good_diffs == 2) # gdq[:, :, row2gd, col2gd] = dqflags["JUMP_DET"] - for idx in row2gd.shape: - if np.max(ratio[:, :, row2gd[idx], col2gd[idx]]) > two_diff_rej_thresh: - gdq[:, :, row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] + for idx in range(row2gd.shape[0]): + if np.max(ratio[:, :, row2gd[idx], col2gd[idx]]) < two_diff_rej_thresh: + gdq[:, :, row2gd[idx], col2gd[idx]] = np.bitwise_and(gdq[:, :, row2gd[idx], col2gd[idx]], + [0b1111111111111111111111111111011]) # gdq[int2gd[idx], np.argmax(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] # gdq[int2gd[idx], np.argmin(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = 0 else: # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups - int4cr, row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, - ratio > normal_rej_thresh)) - int3cr, row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, - ratio > three_diff_rej_thresh)) - - int2cr, row2cr, col2cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 2, - ratio > two_diff_rej_thresh)) - - # get the rows, col pairs for all pixels with at least one CR - all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) - all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) - - # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs - # flag and clip the first CR found. recompute median/sigma/ratio - # and repeat the above steps of comparing the max 'ratio' for each pixel - # to the threshold to determine if another CR can be flagged and clipped. - # repeat this process until no more CRs are found. - for j in range(len(all_crs_row)): - # get arrays of abs(diffs), ratio, readnoise for this pixel - pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] - pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] - pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] - - # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR - pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) - - # set the largest ratio as a CR - pix_cr_mask[np.nanargmax(pix_ratio)] = 0 - new_CR_found = True - - # loop and check for more CRs, setting the mask as you go and - # clipping the group with the CR. stop when no more CRs are found - # or there is only one two diffs left (which means there is - # actually one left, since the next CR will be masked after - # checking that condition) - while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): - - new_CR_found = False - - # set CRs to nans in first diffs to clip them - pix_first_diffs[~pix_cr_mask] = np.nan - - # recalculate median, sigma, and ratio - new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) - - new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) - new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma - - # check if largest ratio exceeds threhold appropriate for num remaining groups - - # select appropriate thresh. based on number of remaining groups - rej_thresh = normal_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: - rej_thresh = three_diff_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: - rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio - if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: - new_CR_found = True - pix_cr_mask[new_pix_max_ratio_idx] = 0 - unusable_diffs = np.sum(np.isnan(pix_first_diffs)) - # Found all CRs for this pix - set flags in input DQ array - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = \ - np.bitwise_or(gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], - dqflags["JUMP_DET"] * np.invert(pix_cr_mask)) +# int4cr, row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, +# ratio > normal_rej_thresh)) +# int3cr, row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, +# ratio > three_diff_rej_thresh)) + if nints == 2: + diff_of_diffs = np.abs(first_diffs[0, :, :, :] - first_diffs[1, :, :, :]) + grp2cr, row2cr, col2cr = np.where(diff_of_diffs > two_diff_rej_thresh) + for idx in range(row2cr.shape[0]): + if first_diffs[0, 0, row2cr[idx], col2cr[idx]] > first_diffs[1, 0, row2cr[idx], col2cr[idx]]: + gdq[0, 1, row2cr[idx], col2cr[idx]] = \ + np.bitwise_or(gdq[0, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) + else: + gdq[1, 1, row2cr[idx], col2cr[idx]] = \ + np.bitwise_or(gdq[1, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) + else: + diff_of_diffs = np.abs(first_diffs[0, 0, :, :] - first_diffs[0, 1, :, :]) + row2cr, col2cr = np.where(diff_of_diffs > two_diff_rej_thresh) + for idx in range(row2cr.shape[0]): + if first_diffs[0, 0, row2cr[idx], col2cr[idx]] > first_diffs[0, 1, row2cr[idx], col2cr[idx]]: + gdq[0, 1, row2cr[idx], col2cr[idx]] = \ + np.bitwise_or(gdq[0, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) + else: + gdq[0, 2, row2cr[idx], col2cr[idx]] = \ + np.bitwise_or(gdq[0, 1, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) + + # row2cr, col2cr = np.where(np.logical_and(diff_of_diffs > two_diff_rej_thresh)) + # r100 = ratio[0, :, 100, 100] + # for idx in range(row2cr.shape[0]): + # if np.max(first_diffs[int2cr[idx], grp2cr[idx], row2cr[idx], col2cr[idx]] + # # get the rows, col pairs for all pixels with at least one CR (only 2 diffs get here) + # all_crs_int = int2cr + # all_crs_grp = grp2cr + # all_crs_row = row2cr + # all_crs_col = col2cr + + + # Found all CRs for this pix - set flags in input DQ array + # gdq[integ, 1:, all_crs_row, all_crs_col] = \ + # np.bitwise_or(gdq[int2cr, all_crs_grp, all_crs_row, all_crs_col], dqflags["JUMP_DET"]) cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index db95825e..7d9af091 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -93,13 +93,31 @@ def test_3grps_cr2_noflux(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) data[0, 0, 100, 100] = 10.0 data[0, 1:4, 100, 100] = 1000 + data[0, 0, 99, 99] = 10.0 + data[0, 2:4, 99, 99] = 1000 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found # assert(1,np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group assert(np.array_equal([0, 4, 0], out_gdq[0, :, 100, 100])) + assert (np.array_equal([0, 0, 4], out_gdq[0, :, 99, 99])) +def test_2ints_2grps_noflux(setup_cube): + ngroups = 2 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2) + data[0, 0, 100, 100] = 10.0 + data[0, 1:3, 100, 100] = 1000 + data[1, 0, 99, 99] = 10.0 + data[1, 1:3, 99, 99] = 1000 + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, + rej_threshold, rej_threshold, nframes, + False, 200, 10, DQFLAGS, + minimum_groups=2) + # assert(4 == np.max(out_gdq)) # a CR was found + # assert(1,np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group + assert(np.array_equal([0, 4], out_gdq[0, :, 100, 100])) + assert (np.array_equal([0, 4], out_gdq[1, :, 99, 99])) def test_4grps_cr2_noflux(setup_cube): ngroups = 4 @@ -127,7 +145,8 @@ def test_6grps_cr2_nframe2(setup_cube): rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found - assert(np.array_equal([0, 4, 4, 0, 0, 0, 0], out_gdq[0, :, 1, 1])) + value = out_gdq[0, :, 1, 1] + assert(np.array_equal([0, 4, 4, 0, 0, 0], out_gdq[0, :, 1, 1])) @pytest.mark.xfail @@ -575,15 +594,25 @@ def test_5grps_satat4_crat3(setup_cube): data[0, 4, 1, 1] = 61000 gdq[0, 3, 1, 1] = DQFLAGS['SATURATED'] gdq[0, 4, 1, 1] = DQFLAGS['SATURATED'] + + data[0, 0, 0, 1] = 59800 + data[0, 1, 0, 1] = 59900 + data[0, 2, 0, 1] = 60000 + data[0, 3, 0, 1] = 61000 + data[0, 4, 0, 1] = 61000 + gdq[0, 3, 0, 1] = DQFLAGS['SATURATED'] + gdq[0, 4, 0, 1] = DQFLAGS['SATURATED'] out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) # assert(4 == np.max(out_gdq)) # no CR was found result = out_gdq[0, :, 1, 1] assert np.array_equal( - [0, 0, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], + [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], out_gdq[0, :, 1, 1]) - + assert np.array_equal( + [0, 0, 0, DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], + out_gdq[0, :, 0, 1]) def test_6grps_satat6_crat1(setup_cube): ngroups = 6 From 68dc2d2a49ee4beb2acbdde4947a8a8a240b26f6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 13 Feb 2024 10:48:02 -0500 Subject: [PATCH 163/196] update --- src/stcal/jump/twopoint_difference.py | 16 +++++++++++++--- tests/test_twopoint_difference.py | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 5d2829d1..ebdde909 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -2,6 +2,7 @@ import numpy as np import astropy.stats as stats import warnings +from astropy.io import fits log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -223,6 +224,12 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # calc. the median of first_diffs for each pixel along the group axis # median_diffs = calc_med_first_diffs(first_diffs) +# median_diffs_old = np.nanmedian(first_diffs, axis=(0, 1)) +# max_index = np.argmax(first_diffs, axis=1) +# print("max index shape", max_index.shape) +# fits.writeto("max_index.fits", max_index, overwrite=True) +# a = max_index[0, 0, 100, 100] +# first_diffs[np.newaxis, np.newaxis, max_index[0], max_index[1]] = np.nan median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -240,15 +247,18 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) # max_ratio = np.nanmax(ratio, axis=0) # warnings.resetwarnings() + med100 = median_diffs[100, 100] e100 = e_jump[:, :, 100, 100] rall100 = ratio_all[:, :, 100, 100] r100 = ratio[:, :, 100, 100] f100 = first_diffs[:, :, 100, 100] num_unusable_diffs = np.sum(np.isnan(first_diffs), axis=(0, 1)) if total_diffs > 2: # enough diffs to calculate the outliers - pix_cr_mask = np.zeros(ratio.shape, dtype=bool) - pix_flagged = np.zeros(dat.shape, dtype=bool) - jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) + if total_diffs >= minimum_groups_median: + else: + pix_cr_mask = np.zeros(ratio.shape, dtype=bool) + pix_flagged = np.zeros(dat.shape, dtype=bool) + jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 pix_flagged[:, 1:, :, :] = pix_cr_mask gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 7d9af091..dbbd8a4f 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -185,9 +185,9 @@ def test_5grps_twocrs_2nd_5thbig(setup_cube): data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) nframes = 1 data[0, 0, 100, 100] = 10.0 - data[0, 1, 100, 100] = 60 - data[0, 2, 100, 100] = 60 - data[0, 3, 100, 100] = 60 + data[0, 1, 100, 100] = 600 + data[0, 2, 100, 100] = 600 + data[0, 3, 100, 100] = 600 data[0, 4, 100, 100] = 2115 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, From d1deff6fb5a1d8af072cc8f736fa64f7ba9b5de0 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Feb 2024 11:45:14 -0500 Subject: [PATCH 164/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 30 +++++++++++---------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index ebdde909..d6dafeaf 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -16,7 +16,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=100, - only_use_ints=True): + only_use_ints=True, min_grps_single_pass=10): """ Find CRs/Jumps in each integration within the input data array. The input @@ -230,32 +230,26 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # fits.writeto("max_index.fits", max_index, overwrite=True) # a = max_index[0, 0, 100, 100] # first_diffs[np.newaxis, np.newaxis, max_index[0], max_index[1]] = np.nan - median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) - # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - # reset sigma so pixels with 0 read noise are not flagged as jumps - sigma[np.where(sigma == 0.)] = np.nan + if total_diffs >= min_grps_single_pass: + median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) + # calculate sigma for each pixel + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + # reset sigma so pixels with 0 read noise are not flagged as jumps + sigma[np.where(sigma == 0.)] = np.nan # compute 'ratio' for each group. this is the value that will be # compared to 'threshold' to classify jumps. subtract the median of # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, np.newaxis, :, :] - - ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] + e_jump = first_diffs - median_diffs[np.newaxis, np.newaxis, :, :] + ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] + masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) # create a 2d array containing the value of the largest 'ratio' for each group # warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) # max_ratio = np.nanmax(ratio, axis=0) # warnings.resetwarnings() - med100 = median_diffs[100, 100] - e100 = e_jump[:, :, 100, 100] - rall100 = ratio_all[:, :, 100, 100] - r100 = ratio[:, :, 100, 100] - f100 = first_diffs[:, :, 100, 100] num_unusable_diffs = np.sum(np.isnan(first_diffs), axis=(0, 1)) - if total_diffs > 2: # enough diffs to calculate the outliers - if total_diffs >= minimum_groups_median: - else: + else: # low number of diffs requires iterative flagging pix_cr_mask = np.zeros(ratio.shape, dtype=bool) pix_flagged = np.zeros(dat.shape, dtype=bool) jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) @@ -301,7 +295,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, gdq[0, 2, row2cr[idx], col2cr[idx]] = \ np.bitwise_or(gdq[0, 1, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) - # row2cr, col2cr = np.where(np.logical_and(diff_of_diffs > two_diff_rej_thresh)) + row2cr, col2cr = np.where(np.logical_and(diff_of_diffs > two_diff_rej_thresh)) # r100 = ratio[0, :, 100, 100] # for idx in range(row2cr.shape[0]): # if np.max(first_diffs[int2cr[idx], grp2cr[idx], row2cr[idx], col2cr[idx]] From 5b8394b4e87bcdf3713f3317738f49cf7cc12e92 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 14 Feb 2024 11:58:33 -0500 Subject: [PATCH 165/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 106 +++++++++++++++++++++----- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index d6dafeaf..48a92d13 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -222,15 +222,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) - # calc. the median of first_diffs for each pixel along the group axis -# median_diffs = calc_med_first_diffs(first_diffs) -# median_diffs_old = np.nanmedian(first_diffs, axis=(0, 1)) -# max_index = np.argmax(first_diffs, axis=1) -# print("max index shape", max_index.shape) -# fits.writeto("max_index.fits", max_index, overwrite=True) -# a = max_index[0, 0, 100, 100] -# first_diffs[np.newaxis, np.newaxis, max_index[0], max_index[1]] = np.nan - if total_diffs >= min_grps_single_pass: + if total_diffs >= min_grps_single_pass: median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -244,18 +236,90 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) - # create a 2d array containing the value of the largest 'ratio' for each group -# warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) -# max_ratio = np.nanmax(ratio, axis=0) -# warnings.resetwarnings() - num_unusable_diffs = np.sum(np.isnan(first_diffs), axis=(0, 1)) - else: # low number of diffs requires iterative flagging - pix_cr_mask = np.zeros(ratio.shape, dtype=bool) - pix_flagged = np.zeros(dat.shape, dtype=bool) - jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) - pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 - pix_flagged[:, 1:, :, :] = pix_cr_mask - gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) + + else:# low number of diffs requires iterative flagging + + # create a 2d array containing the value of the largest 'ratio' for each group + warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) + max_ratio = np.nanmax(ratio, axis=0) + warnings.resetwarnings() + # now see if the largest ratio of all groups for each pixel exceeds the threshold. + # there are different threshold for 4+, 3, and 2 usable groups + num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) + row4cr, col4cr = np.where( + np.logical_and(ndiffs - num_unusable_groups >= 4, max_ratio > normal_rej_thresh) + ) + row3cr, col3cr = np.where( + np.logical_and(ndiffs - num_unusable_groups == 3, max_ratio > three_diff_rej_thresh) + ) + row2cr, col2cr = np.where( + np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh) + ) + + # get the rows, col pairs for all pixels with at least one CR + all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) + all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) + + # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs + # flag and clip the first CR found. recompute median/sigma/ratio + # and repeat the above steps of comparing the max 'ratio' for each pixel + # to the threshold to determine if another CR can be flagged and clipped. + # repeat this process until no more CRs are found. + for j in range(len(all_crs_row)): + # get arrays of abs(diffs), ratio, readnoise for this pixel + pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] + pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] + pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] + + # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR + pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) + + # set the largest ratio as a CR + pix_cr_mask[np.nanargmax(pix_ratio)] = 0 + new_CR_found = True + + # loop and check for more CRs, setting the mask as you go and + # clipping the group with the CR. stop when no more CRs are found + # or there is only one two diffs left (which means there is + # actually one left, since the next CR will be masked after + # checking that condition) + while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): + new_CR_found = False + + # set CRs to nans in first diffs to clip them + pix_first_diffs[~pix_cr_mask] = np.nan + + # recalculate median, sigma, and ratio + new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) + + new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) + new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma + + # check if largest ratio exceeds threshold appropriate for num remaining groups + + # select appropriate thresh. based on number of remaining groups + rej_thresh = normal_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: + rej_thresh = three_diff_rej_thresh + if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: + rej_thresh = two_diff_rej_thresh + new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: + new_CR_found = True + pix_cr_mask[new_pix_max_ratio_idx] = 0 + unusable_diffs = np.sum(np.isnan(pix_first_diffs)) + # Found all CRs for this pix - set flags in input DQ array + gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = np.bitwise_or( + gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], + dqflags["JUMP_DET"] * np.invert(pix_cr_mask), + ) + + pix_cr_mask = np.zeros(ratio.shape, dtype=bool) + pix_flagged = np.zeros(dat.shape, dtype=bool) + jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) + pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 + pix_flagged[:, 1:, :, :] = pix_cr_mask + gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) # deal with pixels with only two good diffs good_diffs = np.ones(shape=(nrows, ncols)) * total_diffs - num_unusable_diffs row2gd, col2gd = np.where(good_diffs == 2) From c13e8780a89e2c754e688a4f201b2f4d2e34af95 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 15 Feb 2024 11:17:29 -0500 Subject: [PATCH 166/196] updates --- src/stcal/jump/twopoint_difference.py | 174 +++++++++++--------------- tests/test_twopoint_difference.py | 15 ++- 2 files changed, 84 insertions(+), 105 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 48a92d13..f6fd3249 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -146,6 +146,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, total_sigclip_groups = nints * (ngrps - num_flagged_grps) total_groups = nints * (ngrps - num_flagged_grps) total_diffs = nints * (ngrps - 1 - num_flagged_grps) + total_usable_diffs = total_diffs - num_flagged_grps if (ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or \ (not only_use_ints and nints * ngrps < minimum_sigclip_groups and total_groups < minimum_groups): @@ -222,7 +223,7 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) - if total_diffs >= min_grps_single_pass: + if total_usable_diffs >= min_grps_single_pass: median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) @@ -238,25 +239,43 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) else:# low number of diffs requires iterative flagging + # calculate the differences between adjacent groups (first diffs) + # use mask on data, so the results will have sat/donotuse groups masked + first_diffs = np.diff(dat, axis=1) - # create a 2d array containing the value of the largest 'ratio' for each group + # calc. the median of first_diffs for each pixel along the group axis + median_diffs = calc_med_first_diffs(first_diffs) + + # calculate sigma for each pixel + sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) + # reset sigma so pxels with 0 readnoise are not flagged as jumps + sigma[np.where(sigma == 0.0)] = np.nan + + # compute 'ratio' for each group. this is the value that will be + # compared to 'threshold' to classify jumps. subtract the median of + # first_diffs from first_diffs, take the abs. value and divide by sigma. + e_jump = first_diffs - median_diffs[np.newaxis, :, :] + ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] + + # create a 2d array containing the value of the largest 'ratio' for each pixel warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) - max_ratio = np.nanmax(ratio, axis=0) + max_ratio = np.nanmax(ratio, axis=1) warnings.resetwarnings() # now see if the largest ratio of all groups for each pixel exceeds the threshold. # there are different threshold for 4+, 3, and 2 usable groups - num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) - row4cr, col4cr = np.where( + num_unusable_groups = np.sum(np.isnan(first_diffs), axis=(0, 1)) + int4cr, row4cr, col4cr = np.where( np.logical_and(ndiffs - num_unusable_groups >= 4, max_ratio > normal_rej_thresh) ) - row3cr, col3cr = np.where( + int3cr, row3cr, col3cr = np.where( np.logical_and(ndiffs - num_unusable_groups == 3, max_ratio > three_diff_rej_thresh) ) - row2cr, col2cr = np.where( + int2cr, row2cr, col2cr = np.where( np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh) ) # get the rows, col pairs for all pixels with at least one CR +# all_crs_int = np.concatenate((int4cr, int3cr, int2cr)) all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) @@ -267,15 +286,27 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, # repeat this process until no more CRs are found. for j in range(len(all_crs_row)): # get arrays of abs(diffs), ratio, readnoise for this pixel - pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] - pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] + TEMP_first_DIFFS = first_diffs[:, :, all_crs_row[j], all_crs_col[j]] + pix_first_diffs = first_diffs[:, :, all_crs_row[j], all_crs_col[j]] + pix_ratio = ratio[:, :, all_crs_row[j], all_crs_col[j]] pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) # set the largest ratio as a CR - pix_cr_mask[np.nanargmax(pix_ratio)] = 0 + test = np.nanargmax(pix_ratio) + int = (test + 1)//ngrps + grp = (test + 1) - int*ngrps + shape = test.shape + print("test size", test.shape) + location = np.unravel_index(pix_ratio.argmax(), pix_ratio.shape) +# if test.shape == (1, 1): +# index = np.zeros((1, 1), dtype=np.int32) +# index[0, 0] = np.nanargmax(pix_ratio) + + # pix_cr_mask[test] = 0 + pix_cr_mask[location] = 0 new_CR_found = True # loop and check for more CRs, setting the mask as you go and @@ -303,77 +334,18 @@ def find_crs(dataa, group_dq, read_noise, normal_rej_thresh, rej_thresh = three_diff_rej_thresh if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio - if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: + location = np.unravel_index(new_pix_ratio.argmax(), new_pix_ratio.shape) + # mask[location] = False + # new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio + if new_pix_ratio[location] > rej_thresh: new_CR_found = True - pix_cr_mask[new_pix_max_ratio_idx] = 0 + pix_cr_mask[location] = 0 unusable_diffs = np.sum(np.isnan(pix_first_diffs)) # Found all CRs for this pix - set flags in input DQ array - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = np.bitwise_or( - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], + gdq[:, 1:, all_crs_row[j], all_crs_col[j]] = np.bitwise_or( + gdq[:, 1:, all_crs_row[j], all_crs_col[j]], dqflags["JUMP_DET"] * np.invert(pix_cr_mask), ) - - pix_cr_mask = np.zeros(ratio.shape, dtype=bool) - pix_flagged = np.zeros(dat.shape, dtype=bool) - jumpint, jumpgrp, jumprow, jumpcol = np.where(ratio > normal_rej_thresh) - pix_cr_mask[jumpint, jumpgrp, jumprow, jumpcol] = 1 - pix_flagged[:, 1:, :, :] = pix_cr_mask - gdq = np.bitwise_or(gdq, dqflags["JUMP_DET"] * pix_flagged) - # deal with pixels with only two good diffs - good_diffs = np.ones(shape=(nrows, ncols)) * total_diffs - num_unusable_diffs - row2gd, col2gd = np.where(good_diffs == 2) -# gdq[:, :, row2gd, col2gd] = dqflags["JUMP_DET"] - for idx in range(row2gd.shape[0]): - if np.max(ratio[:, :, row2gd[idx], col2gd[idx]]) < two_diff_rej_thresh: - gdq[:, :, row2gd[idx], col2gd[idx]] = np.bitwise_and(gdq[:, :, row2gd[idx], col2gd[idx]], - [0b1111111111111111111111111111011]) -# gdq[int2gd[idx], np.argmax(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = dqflags["JUMP_DET"] -# gdq[int2gd[idx], np.argmin(grp2gd[0], grp2gd[1]), row2gd[idx], col2gd[idx]] = 0 - else: - # now see if the largest ratio of all groups for each pixel exceeds the threshold. - # there are different threshold for 4+, 3, and 2 usable groups - -# int4cr, row4cr, col4cr = np.where(np.logical_and(ndiffs - num_unusable_groups >= 4, -# ratio > normal_rej_thresh)) -# int3cr, row3cr, col3cr = np.where(np.logical_and(ndiffs - num_unusable_groups == 3, -# ratio > three_diff_rej_thresh)) - if nints == 2: - diff_of_diffs = np.abs(first_diffs[0, :, :, :] - first_diffs[1, :, :, :]) - grp2cr, row2cr, col2cr = np.where(diff_of_diffs > two_diff_rej_thresh) - for idx in range(row2cr.shape[0]): - if first_diffs[0, 0, row2cr[idx], col2cr[idx]] > first_diffs[1, 0, row2cr[idx], col2cr[idx]]: - gdq[0, 1, row2cr[idx], col2cr[idx]] = \ - np.bitwise_or(gdq[0, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) - else: - gdq[1, 1, row2cr[idx], col2cr[idx]] = \ - np.bitwise_or(gdq[1, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) - else: - diff_of_diffs = np.abs(first_diffs[0, 0, :, :] - first_diffs[0, 1, :, :]) - row2cr, col2cr = np.where(diff_of_diffs > two_diff_rej_thresh) - for idx in range(row2cr.shape[0]): - if first_diffs[0, 0, row2cr[idx], col2cr[idx]] > first_diffs[0, 1, row2cr[idx], col2cr[idx]]: - gdq[0, 1, row2cr[idx], col2cr[idx]] = \ - np.bitwise_or(gdq[0, 0, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) - else: - gdq[0, 2, row2cr[idx], col2cr[idx]] = \ - np.bitwise_or(gdq[0, 1, row2cr[idx], col2cr[idx]], dqflags["JUMP_DET"]) - - row2cr, col2cr = np.where(np.logical_and(diff_of_diffs > two_diff_rej_thresh)) - # r100 = ratio[0, :, 100, 100] - # for idx in range(row2cr.shape[0]): - # if np.max(first_diffs[int2cr[idx], grp2cr[idx], row2cr[idx], col2cr[idx]] - # # get the rows, col pairs for all pixels with at least one CR (only 2 diffs get here) - # all_crs_int = int2cr - # all_crs_grp = grp2cr - # all_crs_row = row2cr - # all_crs_col = col2cr - - - # Found all CRs for this pix - set flags in input DQ array - # gdq[integ, 1:, all_crs_row, all_crs_col] = \ - # np.bitwise_or(gdq[int2cr, all_crs_grp, all_crs_row, all_crs_col], dqflags["JUMP_DET"]) - cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel @@ -476,7 +448,7 @@ def calc_med_first_diffs(first_diffs): ----------- first_diffs : array, float array containing the first differences of adjacent groups - for a single integration. Can be 3d or 1d (for a single pix) + for all integrations. Can be 4d or 1d (for a single pix) Returns ------- @@ -487,51 +459,55 @@ def calc_med_first_diffs(first_diffs): will be returned. """ - if first_diffs.ndim == 1: # in the case where input is a single pixel - - num_usable_groups = len(first_diffs) - np.sum(np.isnan(first_diffs), axis=0) - if num_usable_groups >= 4: # if 4+, clip largest and return median + if first_diffs.ndim == 2: # in the case where input is a single pixel + nansum = np.sum(np.isnan(first_diffs), axis=(0, 1)) + num_usable_diffs = first_diffs.size - np.sum(np.isnan(first_diffs), axis=(0, 1)) + if num_usable_diffs >= 4: # if 4+, clip largest and return median mask = np.ones_like(first_diffs).astype(bool) - mask[np.nanargmax(np.abs(first_diffs))] = False # clip the diff with the largest abs value + location = np.unravel_index(first_diffs.argmax(), first_diffs.shape) + mask[location] = False # clip the diff with the largest abs value return np.nanmedian(first_diffs[mask]) - elif num_usable_groups == 3: # if 3, no clipping just return median + elif num_usable_diffs == 3: # if 3, no clipping just return median return np.nanmedian(first_diffs) - elif num_usable_groups == 2: # if 2, return diff with minimum abs - return first_diffs[np.nanargmin(np.abs(first_diffs))] + elif num_usable_diffs == 2: # if 2, return diff with minimum abs + TEST = np.nanargmin(np.abs(first_diffs)) + location = np.unravel_index(first_diffs.argmin(), first_diffs.shape) + return first_diffs[location] else: return np.nan # if input is multi-dimensional - - ngroups, nrows, ncols = first_diffs.shape - num_usable_groups = ngroups - np.sum(np.isnan(first_diffs), axis=0) + nints, ndiffs, nrows, ncols = first_diffs.shape + shaped_diffs = np.reshape(first_diffs, ((nints * ndiffs), nrows, ncols)) + num_usable_diffs = ndiffs - np.sum(np.isnan(shaped_diffs), axis=1) median_diffs = np.zeros((nrows, ncols)) # empty array to store median for each pix - # process groups with >=4 usable groups - row4, col4 = np.where(num_usable_groups >= 4) # locations of >= 4 usable group pixels + # process groups with >=4 usable diffs + row4, col4 = np.where(num_usable_diffs >= 4) # locations of >= 4 usable diffs pixels if len(row4) > 0: - four_slice = first_diffs[:, row4, col4] - four_slice[np.nanargmax(np.abs(four_slice), axis=0), - np.arange(four_slice.shape[1])] = np.nan # mask largest group in slice + four_slice = first_diffs[:, :, row4, col4] + location = np.unravel_index(four_slice.argmax(), four_slice.shape) + four_slice[location] = np.nan # mask largest group in slice + hold = np.nanmedian(four_slice, axis=1) median_diffs[row4, col4] = np.nanmedian(four_slice, axis=0) # add median to return arr for these pix # process groups with 3 usable groups - row3, col3 = np.where(num_usable_groups == 3) # locations of >= 4 usable group pixels + row3, col3 = np.where(num_usable_diffs == 3) # locations of == 3 usable diff pixels if len(row3) > 0: - three_slice = first_diffs[:, row3, col3] + three_slice = shaped_diffs[:, row3, col3] median_diffs[row3, col3] = np.nanmedian(three_slice, axis=0) # add median to return arr for these pix # process groups with 2 usable groups - row2, col2 = np.where(num_usable_groups == 2) # locations of >= 4 usable group pixels + row2, col2 = np.where(num_usable_diffs == 2) # locations of == 2 usable diff pixels if len(row2) > 0: - two_slice = first_diffs[:, row2, col2] + two_slice = shaped_diffs[ :, row2, col2] two_slice[np.nanargmax(np.abs(two_slice), axis=0), np.arange(two_slice.shape[1])] = np.nan # mask larger abs. val median_diffs[row2, col2] = np.nanmin(two_slice, axis=0) # add med. to return arr - # set the medians all groups with less than 2 usable groups to nan to skip further + # set the medians all groups with less than 2 usable diffs to nan to skip further # calculations for these pixels - row_none, col_none = np.where(num_usable_groups < 2) + row_none, col_none = np.where(num_usable_diffs < 2) median_diffs[row_none, col_none] = np.nan return median_diffs diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index dbbd8a4f..c185dc69 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -43,20 +43,23 @@ def test_5grps_cr3_noflux(setup_cube): rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found + out_mask = out_gdq[0, :, 100, 100] assert(2 == np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group -def test_5grps_cr2_noflux(setup_cube): - ngroups = 5 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) - data[0, 0, 100, 100] = 10.0 - data[0, 1:6, 100, 100] = 1000 + +def test_4grps_2ints_cr2_noflux(setup_cube): + ngroups = 5 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2) + data[0, 1, 100,100] = 5 + data[1, 0, 100, 100] = 10.0 + data[1, 1:6, 100, 100] = 1000 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) assert(4 == np.max(out_gdq)) # a CR was found - assert(1 == np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group + assert(1 == np.argmax(out_gdq[1, :, 100, 100])) # find the CR in the expected group def test_6grps_negative_differences_zeromedian(setup_cube): From b49adf0d27855bcfae20ab0daffd9f0708ca33c4 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 10:50:29 -0500 Subject: [PATCH 167/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 225 +++----------------------- tests/test_twopoint_difference.py | 98 +++++------ 2 files changed, 75 insertions(+), 248 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 7956f4c9..bb6339b4 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -31,6 +31,7 @@ def find_crs( minimum_groups=3, minimum_sigclip_groups=100, only_use_ints=True, + min_grps_single_pass=10, ): """ Find CRs/Jumps in each integration within the input data array. The input @@ -255,7 +256,7 @@ def find_crs( else:# low number of diffs requires iterative flagging # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dat, axis=1) + first_diffs = np.abs(np.diff(dat, axis=1)) # calc. the median of first_diffs for each pixel along the group axis median_diffs = calc_med_first_diffs(first_diffs) @@ -310,11 +311,12 @@ def find_crs( # set the largest ratio as a CR test = np.nanargmax(pix_ratio) - int = (test + 1)//ngrps - grp = (test + 1) - int*ngrps + int = (test )//ngrps + grp = (test ) - int * ngrps shape = test.shape print("test size", test.shape) - location = np.unravel_index(pix_ratio.argmax(), pix_ratio.shape) + pix_ratio_tmp = np.nanargmax(pix_ratio) + location = np.unravel_index(np.nanargmax(pix_ratio), pix_ratio.shape) # if test.shape == (1, 1): # index = np.zeros((1, 1), dtype=np.int32) # index[0, 0] = np.nanargmax(pix_ratio) @@ -348,7 +350,8 @@ def find_crs( rej_thresh = three_diff_rej_thresh if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: rej_thresh = two_diff_rej_thresh - location = np.unravel_index(new_pix_ratio.argmax(), new_pix_ratio.shape) + max_idx = np.nanargmax(new_pix_ratio) + location = np.unravel_index(max_idx, new_pix_ratio.shape) # mask[location] = False # new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio if new_pix_ratio[location] > rej_thresh: @@ -360,190 +363,7 @@ def find_crs( gdq[:, 1:, all_crs_row[j], all_crs_col[j]], dqflags["JUMP_DET"] * np.invert(pix_cr_mask), ) - cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) - num_primary_crs = len(cr_group) - if flag_4_neighbors: # iterate over each 'jump' pixel -======= - total_groups = nints if only_use_ints and nints else nints * ngrps - num_flagged_grps - if (ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or ( - not only_use_ints and nints * ngrps < minimum_sigclip_groups and ngrps < minimum_groups - ): - log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") - log.info("Data shape %s", dat.shape) - dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), dtype=np.float32) - - return gdq, row_below_gdq, row_above_gdq, 0, dummy - - # set 'saturated' or 'do not use' pixels to nan in data - dat[np.where(np.bitwise_and(gdq, sat_flag))] = np.nan - dat[np.where(np.bitwise_and(gdq, dnu_flag))] = np.nan - dat[np.where(np.bitwise_and(gdq, dnu_flag + sat_flag))] = np.nan - - # calculate the differences between adjacent groups (first diffs) - # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dat, axis=1) - - # calc. the median of first_diffs for each pixel along the group axis - first_diffs_masked = np.ma.masked_array(first_diffs, mask=np.isnan(first_diffs)) - median_diffs = np.ma.median(first_diffs_masked, axis=(0, 1)) - # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - - # reset sigma so pxels with 0 readnoise are not flagged as jumps - sigma[np.where(sigma == 0.0)] = np.nan - - # compute 'ratio' for each group. this is the value that will be - # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] - ratio_all = ( - np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / sigma[np.newaxis, np.newaxis, :, :] - ) - if (only_use_ints and nints >= minimum_sigclip_groups) or ( - not only_use_ints and total_groups >= minimum_sigclip_groups - ): - log.info( - " Jump Step using sigma clip %s greater than %s, rejection threshold %s", - total_groups, - minimum_sigclip_groups, - normal_rej_thresh, - ) - warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) - warnings.filterwarnings("ignore", ".*Mean of empty slice.*", RuntimeWarning) - warnings.filterwarnings("ignore", ".*Degrees of freedom <= 0.*", RuntimeWarning) - - if only_use_ints: - mean, median, stddev = stats.sigma_clipped_stats( - first_diffs_masked, sigma=normal_rej_thresh, axis=0 - ) - clipped_diffs = stats.sigma_clip(first_diffs_masked, sigma=normal_rej_thresh, axis=0, masked=True) - else: - mean, median, stddev = stats.sigma_clipped_stats( - first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1) - ) - clipped_diffs = stats.sigma_clip( - first_diffs_masked, sigma=normal_rej_thresh, axis=(0, 1), masked=True - ) - jump_mask = np.logical_and(clipped_diffs.mask, np.logical_not(first_diffs_masked.mask)) - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False - gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - # if grp is all jump set to do not use - for integ in range(nints): - for grp in range(ngrps): - if np.all( - np.bitwise_or( - np.bitwise_and(gdq[integ, grp, :, :], jump_flag), - np.bitwise_and(gdq[integ, grp, :, :], dnu_flag), - ) - ): - jumpy, jumpx = np.where(gdq[integ, grp, :, :] == jump_flag) - gdq[integ, grp, jumpy, jumpx] = 0 - warnings.resetwarnings() - else: - for integ in range(nints): - # get data, gdq for this integration - dat = dataa[integ] - gdq_integ = gdq[integ] - - # set 'saturated' or 'do not use' pixels to nan in data - dat[np.where(np.bitwise_and(gdq_integ, sat_flag))] = np.nan - dat[np.where(np.bitwise_and(gdq_integ, dnu_flag))] = np.nan - - # calculate the differences between adjacent groups (first diffs) - # use mask on data, so the results will have sat/donotuse groups masked - first_diffs = np.diff(dat, axis=0) - - # calc. the median of first_diffs for each pixel along the group axis - median_diffs = calc_med_first_diffs(first_diffs) - - # calculate sigma for each pixel - sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) - # reset sigma so pxels with 0 readnoise are not flagged as jumps - sigma[np.where(sigma == 0.0)] = np.nan - - # compute 'ratio' for each group. this is the value that will be - # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. - e_jump = first_diffs - median_diffs[np.newaxis, :, :] - ratio = np.abs(e_jump) / sigma[np.newaxis, :, :] - - # create a 2d array containing the value of the largest 'ratio' for each group - warnings.filterwarnings("ignore", ".*All-NaN slice encountered.*", RuntimeWarning) - max_ratio = np.nanmax(ratio, axis=0) - warnings.resetwarnings() - # now see if the largest ratio of all groups for each pixel exceeds the threshold. - # there are different threshold for 4+, 3, and 2 usable groups - num_unusable_groups = np.sum(np.isnan(first_diffs), axis=0) - row4cr, col4cr = np.where( - np.logical_and(ndiffs - num_unusable_groups >= 4, max_ratio > normal_rej_thresh) - ) - row3cr, col3cr = np.where( - np.logical_and(ndiffs - num_unusable_groups == 3, max_ratio > three_diff_rej_thresh) - ) - row2cr, col2cr = np.where( - np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh) - ) - - # get the rows, col pairs for all pixels with at least one CR - all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) - all_crs_col = np.concatenate((col4cr, col3cr, col2cr)) - - # iterate over all groups of the pix w/ an initial CR to look for subsequent CRs - # flag and clip the first CR found. recompute median/sigma/ratio - # and repeat the above steps of comparing the max 'ratio' for each pixel - # to the threshold to determine if another CR can be flagged and clipped. - # repeat this process until no more CRs are found. - for j in range(len(all_crs_row)): - # get arrays of abs(diffs), ratio, readnoise for this pixel - pix_first_diffs = first_diffs[:, all_crs_row[j], all_crs_col[j]] - pix_ratio = ratio[:, all_crs_row[j], all_crs_col[j]] - pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] - - # Create a mask to flag CRs. pix_cr_mask = 0 denotes a CR - pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) - - # set the largest ratio as a CR - pix_cr_mask[np.nanargmax(pix_ratio)] = 0 - new_CR_found = True - - # loop and check for more CRs, setting the mask as you go and - # clipping the group with the CR. stop when no more CRs are found - # or there is only one two diffs left (which means there is - # actually one left, since the next CR will be masked after - # checking that condition) - while new_CR_found and (ndiffs - np.sum(np.isnan(pix_first_diffs)) > 2): - new_CR_found = False - - # set CRs to nans in first diffs to clip them - pix_first_diffs[~pix_cr_mask] = np.nan - - # recalculate median, sigma, and ratio - new_pix_median_diffs = calc_med_first_diffs(pix_first_diffs) - - new_pix_sigma = np.sqrt(np.abs(new_pix_median_diffs) + pix_rn2 / nframes) - new_pix_ratio = np.abs(pix_first_diffs - new_pix_median_diffs) / new_pix_sigma - - # check if largest ratio exceeds threshold appropriate for num remaining groups - - # select appropriate thresh. based on number of remaining groups - rej_thresh = normal_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 3: - rej_thresh = three_diff_rej_thresh - if ndiffs - np.sum(np.isnan(pix_first_diffs)) == 2: - rej_thresh = two_diff_rej_thresh - new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio - if new_pix_ratio[new_pix_max_ratio_idx] > rej_thresh: - new_CR_found = True - pix_cr_mask[new_pix_max_ratio_idx] = 0 - unusable_diffs = np.sum(np.isnan(pix_first_diffs)) - # Found all CRs for this pix - set flags in input DQ array - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]] = np.bitwise_or( - gdq[integ, 1:, all_crs_row[j], all_crs_col[j]], - dqflags["JUMP_DET"] * np.invert(pix_cr_mask), - ) - + fits.writeto("outgdq.fits", gdq, overwrite=True) cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel @@ -649,9 +469,6 @@ def calc_med_first_diffs(first_diffs): those three groups will be returned without any clipping. Finally, if there are two usable groups, the group with the smallest absolute difference will be returned. ->>>>>>> 75e1efacba98506604174702b747e7586e9fbc95 - -<<<<<<< HEAD Parameters ---------- first_diffs : array, float @@ -666,9 +483,7 @@ def calc_med_first_diffs(first_diffs): array of several pixels, a 2d array with the median for each pixel will be returned. """ - if first_diffs.ndim == 1: # in the case where input is a single pixel - num_usable_groups = len(first_diffs) - np.sum(np.isnan(first_diffs), axis=0) - if num_usable_groups >= 4: # if 4+, clip largest and return median + if first_diffs.ndim == 2: # in the case where input is a single pixel nansum = np.sum(np.isnan(first_diffs), axis=(0, 1)) num_usable_diffs = first_diffs.size - np.sum(np.isnan(first_diffs), axis=(0, 1)) @@ -681,7 +496,8 @@ def calc_med_first_diffs(first_diffs): return np.nanmedian(first_diffs) elif num_usable_diffs == 2: # if 2, return diff with minimum abs TEST = np.nanargmin(np.abs(first_diffs)) - location = np.unravel_index(first_diffs.argmin(), first_diffs.shape) + diff_min_idx = np.nanargmin(first_diffs) + location = np.unravel_index(diff_min_idx, first_diffs.shape) return first_diffs[location] else: return np.nan @@ -689,16 +505,23 @@ def calc_med_first_diffs(first_diffs): # if input is multi-dimensional nints, ndiffs, nrows, ncols = first_diffs.shape shaped_diffs = np.reshape(first_diffs, ((nints * ndiffs), nrows, ncols)) - num_usable_diffs = ndiffs - np.sum(np.isnan(shaped_diffs), axis=1) + num_usable_diffs = (ndiffs * nints) - np.sum(np.isnan(shaped_diffs), axis=0) median_diffs = np.zeros((nrows, ncols)) # empty array to store median for each pix # process groups with >=4 usable diffs row4, col4 = np.where(num_usable_diffs >= 4) # locations of >= 4 usable diffs pixels if len(row4) > 0: - four_slice = first_diffs[:, :, row4, col4] - location = np.unravel_index(four_slice.argmax(), four_slice.shape) - four_slice[location] = np.nan # mask largest group in slice - hold = np.nanmedian(four_slice, axis=1) + four_slice = shaped_diffs[:, row4, col4] + tmp = four_slice.argmax(axis=0) + max_values_indx = four_slice.argmax(axis=0) + four_slice[max_values_indx, :] = np.nan + out_slice = np.reshape(four_slice, (shaped_diffs.shape)) +# fits.writeto("outslice.fits", out_slice, overwrite=True) +# location = np.reshape(four_slice.argmax(axis=0), (nrows, ncols)) +# max_values = np.reshape(location, (len(row4), len(col4))) +# four_slice[max_values] = np.nan # mask largest group in slice +# med = np.nanmedian(four_slice, axis=0) +# hold = np.unravel_index(np.nanmedian(four_slice, axis=(0, 1)), (num_usable_diffs.shape[1], num_usable_diffs.shape[2])) median_diffs[row4, col4] = np.nanmedian(four_slice, axis=0) # add median to return arr for these pix # process groups with 3 usable groups diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index e1483eee..f99384fd 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -48,10 +48,10 @@ def test_5grps_cr3_noflux(setup_cube): def test_4grps_2ints_cr2_noflux(setup_cube): ngroups = 5 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2) - data[0, 1, 100,100] = 5 - data[1, 0, 100, 100] = 10.0 - data[1, 1:6, 100, 100] = 1000 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2, ncols=2, nrows=2) + data[0, 1, 1, 1] = 5 + data[1, 0, 1, 1] = 10.0 + data[1, 1:6, 1, 1] = 1000 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS) @@ -60,8 +60,10 @@ def test_4grps_2ints_cr2_noflux(setup_cube): data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) assert np.max(out_gdq) == 4 # a CR was found - assert np.argmax(out_gdq[0, :, 100, 100]) == 1 # find the CR in the expected group - assert(1 == np.argmax(out_gdq[1, :, 100, 100])) # find the CR in the expected group + fits.writeto("outgdq_final.fits", out_gdq, overwrite=True) + gdq_11 = out_gdq[:, :, 1, 1] + assert np.argmax(out_gdq[1, :, 1, 1]) == 1 # find the CR in the expected group + assert(1 == np.argmax(out_gdq[1, :, 1, 1])) # find the CR in the expected group def test_6grps_negative_differences_zeromedian(setup_cube): ngroups = 6 @@ -109,19 +111,19 @@ def test_3grps_cr2_noflux(setup_cube): def test_2ints_2grps_noflux(setup_cube): ngroups = 2 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2) - data[0, 0, 100, 100] = 10.0 - data[0, 1:3, 100, 100] = 1000 - data[1, 0, 99, 99] = 10.0 - data[1, 1:3, 99, 99] = 1000 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2, ncols=2, nrows=2) + data[0, 0, 1, 1] = 10.0 + data[0, 1:3, 1, 1] = 1000 + data[1, 0, 0, 0] = 10.0 + data[1, 1:3, 0, 0] = 1000 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs(data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS, minimum_groups=2) # assert(4 == np.max(out_gdq)) # a CR was found # assert(1,np.argmax(out_gdq[0, :, 100, 100])) # find the CR in the expected group - assert(np.array_equal([0, 4], out_gdq[0, :, 100, 100])) - assert (np.array_equal([0, 4], out_gdq[1, :, 99, 99])) + assert(np.array_equal([0, 4], out_gdq[0, :, 1, 1])) + assert (np.array_equal([0, 4], out_gdq[1, :, 0, 0])) def test_4grps_cr2_noflux(setup_cube): ngroups = 4 @@ -175,6 +177,7 @@ def test_5grps_twocrs_2nd_5th(setup_cube): data[0, 2, 100, 100] = 60 data[0, 3, 100, 100] = 60 data[0, 4, 100, 100] = 115 + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) assert np.max(out_gdq) == 4 # a CR was found @@ -317,15 +320,16 @@ def test_5grps_cr2_negslope(setup_cube): ngroups = 5 data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups) nframes = 1 - data[0, 0, 100, 100] = 100.0 - data[0, 1, 100, 100] = 0 - data[0, 2, 100, 100] = -200 - data[0, 3, 100, 100] = -260 - data[0, 4, 100, 100] = -360 + data[0, 0, 1, 1] = 100.0 + data[0, 1, 1, 1] = 0 + data[0, 2, 1, 1] = -200 + data[0, 3, 1, 1] = -260 + data[0, 4, 1, 1] = -360 + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) assert np.max(out_gdq) == 4 # a CR was found - assert np.array_equal([0, 0, 4, 0, 0], out_gdq[0, :, 100, 100]) + assert np.array_equal([0, 0, 4, 0, 0], out_gdq[0, :, 1, 1]) def test_6grps_1cr(setup_cube): @@ -381,22 +385,22 @@ def test_8grps_1cr(setup_cube): def test_9grps_1cr_1sat(setup_cube): ngroups = 9 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10, nrows=2, ncols=2) nframes = 1 - data[0, 0, 100, 100] = 0 - data[0, 1, 100, 100] = 10 - data[0, 2, 100, 100] = 21 - data[0, 3, 100, 100] = 33 - data[0, 4, 100, 100] = 46 - data[0, 5, 100, 100] = 60 - data[0, 6, 100, 100] = 1160 - data[0, 7, 100, 100] = 1175 - data[0, 8, 100, 100] = 6175 - gdq[0, 8, 100, 100] = DQFLAGS["SATURATED"] + data[0, 0, 1, 1] = 0 + data[0, 1, 1, 1] = 10 + data[0, 2, 1, 1] = 21 + data[0, 3, 1, 1] = 33 + data[0, 4, 1, 1] = 46 + data[0, 5, 1, 1] = 60 + data[0, 6, 1, 1] = 1160 + data[0, 7, 1, 1] = 1175 + data[0, 8, 1, 1] = 6175 + gdq[0, 8, 1, 1] = DQFLAGS["SATURATED"] out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) - assert out_gdq[0, 6, 100, 100] == 4 + assert out_gdq[0, 6, 1, 1] == 4 def test_10grps_1cr_2sat(setup_cube): @@ -423,26 +427,26 @@ def test_10grps_1cr_2sat(setup_cube): def test_11grps_1cr_3sat(setup_cube): ngroups = 11 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, readnoise=10, nrows=2, ncols=2) nframes = 1 - data[0, 0, 100, 100] = 0 - data[0, 1, 100, 100] = 20 - data[0, 2, 100, 100] = 39 - data[0, 3, 100, 100] = 57 - data[0, 4, 100, 100] = 74 - data[0, 5, 100, 100] = 90 - data[0, 6, 100, 100] = 1160 - data[0, 7, 100, 100] = 1175 - data[0, 8, 100, 100] = 6175 - data[0, 9, 100, 100] = 6175 - data[0, 10, 100, 100] = 6175 - gdq[0, 8, 100, 100] = DQFLAGS["SATURATED"] - gdq[0, 9, 100, 100] = DQFLAGS["SATURATED"] - gdq[0, 10, 100, 100] = DQFLAGS["SATURATED"] + data[0, 0, 1, 1] = 0 + data[0, 1, 1, 1] = 20 + data[0, 2, 1, 1] = 39 + data[0, 3, 1, 1] = 57 + data[0, 4, 1, 1] = 74 + data[0, 5, 1, 1] = 90 + data[0, 6, 1, 1] = 1160 + data[0, 7, 1, 1] = 1175 + data[0, 8, 1, 1] = 6175 + data[0, 9, 1, 1] = 6175 + data[0, 10, 1, 1] = 6175 + gdq[0, 8, 1, 1] = DQFLAGS["SATURATED"] + gdq[0, 9, 1, 1] = DQFLAGS["SATURATED"] + gdq[0, 10, 1, 1] = DQFLAGS["SATURATED"] out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) - assert out_gdq[0, 6, 100, 100] == 4 + assert out_gdq[0, 6, 1, 1] == 4 def test_11grps_0cr_3donotuse(setup_cube): From 45964e2373904c739fc1685d8001dd57ce9e6279 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 10:59:30 -0500 Subject: [PATCH 168/196] Update jump.py --- src/stcal/jump/jump.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 05e02e57..d6d59241 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -54,6 +54,7 @@ def detect_jumps( minimum_groups=3, minimum_sigclip_groups=100, only_use_ints=True, + min_grps_single_pass=10, ): """ This is the high-level controlling routine for the jump detection process. @@ -276,6 +277,7 @@ def detect_jumps( minimum_groups=3, minimum_sigclip_groups=minimum_sigclip_groups, only_use_ints=only_use_ints, + minimum_primary_crs=min_grps_single_pass, ) # This is the flag that controls the flagging of snowballs. if expand_large_events: @@ -351,6 +353,7 @@ def detect_jumps( minimum_groups, minimum_sigclip_groups, only_use_ints, + min_grps_single_pass, ), ) @@ -377,6 +380,7 @@ def detect_jumps( minimum_groups, minimum_sigclip_groups, only_use_ints, + min_grps_single_pass, ), ) log.info("Creating %d processes for jump detection ", n_slices) From 2076833e0e8c2833d4c6730386048e06d9dfe62e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 12:43:54 -0500 Subject: [PATCH 169/196] Update jump.py --- src/stcal/jump/jump.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index d6d59241..597936aa 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -5,7 +5,6 @@ import numpy as np from astropy import stats from astropy.convolution import Ring2DKernel, convolve ->>>>>>> 75e1efacba98506604174702b747e7586e9fbc95 from . import constants from . import twopoint_difference as twopt From dce95fc60c1e12a5afa48a98fc4ad83e466e88ac Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 14:26:46 -0500 Subject: [PATCH 170/196] fix clipping --- src/stcal/jump/twopoint_difference.py | 7 ++++++- tests/test_twopoint_difference.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index bb6339b4..11d1a575 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -252,7 +252,12 @@ def find_crs( ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) - + jump_mask = np.logical_and(masked_ratio.mask, np.logical_not(first_diffs_masked.mask)) + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False + jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False + gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * + np.uint8(dqflags["JUMP_DET"])) else:# low number of diffs requires iterative flagging # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index f99384fd..d5fccc0f 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -21,6 +21,23 @@ def _cube(ngroups, nints=1, nrows=204, ncols=204, readnoise=10): return _cube +def test_multint_pixel(setup_cube): + ngroups=4 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=7, nrows=2, ncols=2, readnoise=8) + data[0, :, 0, 0] = (-24, -15, 0, 13) + data[1, :, 0, 0] = (-24, -11, 6, 21) + data[2, :, 0, 0] = (-40, -28, -24, -4) + data[3, :, 0, 0] = (-11, 3, 11, 24) + data[4, :, 0, 0] = (-43 , -24, -12, 1) + data[5, :, 0, 0] = (-45, 8537, 17380, 17437) + data[6, :, 0, 0] = (-178, -156, -139, -125) + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( + data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS + ) + assert np.max(out_gdq) == 4 # a CR was found + assert (np.array_equal([0, 4, 4, 4], out_gdq[5, :, 0, 0])) + + def test_nocrs_noflux(setup_cube): ngroups = 5 From 0e99f4004ae027260d055f62737a2ae35bffd775 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 14:38:19 -0500 Subject: [PATCH 171/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 11d1a575..30c85e35 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -258,7 +258,7 @@ def find_crs( jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) - else:# low number of diffs requires iterative flagging + else: # low number of diffs requires iterative flagging # calculate the differences between adjacent groups (first diffs) # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.abs(np.diff(dat, axis=1)) From 27a4b49181d5c5d2c671bbccb2f3d5f43bc9f27f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 19 Feb 2024 15:04:52 -0500 Subject: [PATCH 172/196] new output --- src/stcal/jump/jump.py | 3 +++ src/stcal/jump/twopoint_difference.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 597936aa..92b35d7e 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -3,6 +3,7 @@ import time import cv2 as cv import numpy as np +from astropy.io import fits from astropy import stats from astropy.convolution import Ring2DKernel, convolve @@ -430,6 +431,8 @@ def detect_jumps( # save the neighbors to be flagged that will be in the next slice previous_row_above_gdq = row_above_gdq.copy() k += 1 + fits.writeto("jumpgdq.fits", gdq, overwrite=True) + # This is the flag that controls the flagging of snowballs. if expand_large_events: total_snowballs = flag_large_events( diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 30c85e35..f783cb29 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -253,9 +253,9 @@ def find_crs( ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) jump_mask = np.logical_and(masked_ratio.mask, np.logical_not(first_diffs_masked.mask)) - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False - jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False +# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False +# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False +# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) else: # low number of diffs requires iterative flagging From 1a2f5dbfba7f34f73fd666fb3b19eecc0771601a Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 20 Feb 2024 16:33:48 -0500 Subject: [PATCH 173/196] unit test updates --- src/stcal/jump/twopoint_difference.py | 118 ++++++++++++++++---------- tests/test_twopoint_difference.py | 60 ++++++++----- 2 files changed, 115 insertions(+), 63 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index f783cb29..fc454d76 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -464,7 +464,7 @@ def find_crs( return gdq, row_below_gdq, row_above_gdq, num_primary_crs, dummy -def calc_med_first_diffs(first_diffs): +def calc_med_first_diffs(in_first_diffs): """Calculate the median of `first diffs` along the group axis. If there are 4+ usable groups (e.g not flagged as saturated, donotuse, @@ -476,7 +476,7 @@ def calc_med_first_diffs(first_diffs): difference will be returned. Parameters ---------- - first_diffs : array, float + in_first_diffs : array, float array containing the first differences of adjacent groups for a single integration. Can be 3d or 1d (for a single pix) @@ -488,6 +488,21 @@ def calc_med_first_diffs(first_diffs): array of several pixels, a 2d array with the median for each pixel will be returned. """ + first_diffs = in_first_diffs.copy() + if first_diffs.ndim == 1: # in the case where input is a single pixel + num_usable_groups = len(first_diffs) - np.sum(np.isnan(first_diffs), axis=0) + if num_usable_groups >= 4: # if 4+, clip largest and return median + mask = np.ones_like(first_diffs).astype(bool) + mask[np.nanargmax(np.abs(first_diffs))] = False # clip the diff with the largest abs value + return np.nanmedian(first_diffs[mask]) + + if num_usable_groups == 3: # if 3, no clipping just return median + return np.nanmedian(first_diffs) + + if num_usable_groups == 2: # if 2, return diff with minimum abs + return first_diffs[np.nanargmin(np.abs(first_diffs))] + + return np.nan if first_diffs.ndim == 2: # in the case where input is a single pixel nansum = np.sum(np.isnan(first_diffs), axis=(0, 1)) @@ -507,45 +522,60 @@ def calc_med_first_diffs(first_diffs): else: return np.nan - # if input is multi-dimensional - nints, ndiffs, nrows, ncols = first_diffs.shape - shaped_diffs = np.reshape(first_diffs, ((nints * ndiffs), nrows, ncols)) - num_usable_diffs = (ndiffs * nints) - np.sum(np.isnan(shaped_diffs), axis=0) - median_diffs = np.zeros((nrows, ncols)) # empty array to store median for each pix - - # process groups with >=4 usable diffs - row4, col4 = np.where(num_usable_diffs >= 4) # locations of >= 4 usable diffs pixels - if len(row4) > 0: - four_slice = shaped_diffs[:, row4, col4] - tmp = four_slice.argmax(axis=0) - max_values_indx = four_slice.argmax(axis=0) - four_slice[max_values_indx, :] = np.nan - out_slice = np.reshape(four_slice, (shaped_diffs.shape)) -# fits.writeto("outslice.fits", out_slice, overwrite=True) -# location = np.reshape(four_slice.argmax(axis=0), (nrows, ncols)) -# max_values = np.reshape(location, (len(row4), len(col4))) -# four_slice[max_values] = np.nan # mask largest group in slice -# med = np.nanmedian(four_slice, axis=0) -# hold = np.unravel_index(np.nanmedian(four_slice, axis=(0, 1)), (num_usable_diffs.shape[1], num_usable_diffs.shape[2])) - median_diffs[row4, col4] = np.nanmedian(four_slice, axis=0) # add median to return arr for these pix - - # process groups with 3 usable groups - row3, col3 = np.where(num_usable_diffs == 3) # locations of == 3 usable diff pixels - if len(row3) > 0: - three_slice = shaped_diffs[:, row3, col3] - median_diffs[row3, col3] = np.nanmedian(three_slice, axis=0) # add median to return arr for these pix - - # process groups with 2 usable groups - row2, col2 = np.where(num_usable_diffs == 2) # locations of == 2 usable diff pixels - if len(row2) > 0: - two_slice = shaped_diffs[ :, row2, col2] - two_slice[np.nanargmax(np.abs(two_slice), axis=0), - np.arange(two_slice.shape[1])] = np.nan # mask larger abs. val - median_diffs[row2, col2] = np.nanmin(two_slice, axis=0) # add med. to return arr - - # set the medians all groups with less than 2 usable diffs to nan to skip further - # calculations for these pixels - row_none, col_none = np.where(num_usable_diffs < 2) - median_diffs[row_none, col_none] = np.nan - - return median_diffs + if first_diffs.ndim == 4: + # if input is multi-dimensional + nints, ndiffs, nrows, ncols = first_diffs.shape + shaped_diffs = np.reshape(first_diffs, ((nints * ndiffs), nrows, ncols)) + num_usable_diffs = (ndiffs * nints) - np.sum(np.isnan(shaped_diffs), axis=0) + median_diffs = np.zeros((nrows, ncols)) # empty array to store median for each pix + + # process groups with >=4 usable diffs + row4, col4 = np.where(num_usable_diffs >= 4) # locations of >= 4 usable diffs pixels + if len(row4) > 0: +# mask = np.ones_like(shaped_diffs).astype(bool) +# location = np.unravel_index(shaped_diffs.argmax(), shaped_diffs.shape) +# mask[location] = False # clip the diff with the largest abs value +# masked_first_diffs = shaped_diffs[mask] +# result = np.nanmedian(first_diffs[mask], axis=(0, 1)) +# return np.nanmedian(first_diffs[mask], axis=(0, 1)) + + four_slice = shaped_diffs[:, row4, col4] + loc0 = np.nanargmax(four_slice, axis=0) + tmp1 = np.argmax(shaped_diffs, axis=1) + tmp2 = np.argmax(shaped_diffs, axis=2) + location = np.unravel_index(np.argmax(four_slice, axis=0), (four_slice.shape)) + shaped_diffs[loc0, row4, col4] = np.nan + median_diffs[row4, col4] = np.nanmedian(shaped_diffs[:, row4, col4], axis=0) +# return np.nanmedian(four_slice[:, row4, col4]) +# tmp = four_slice.argmax(axis=0) +# max_values_indx = four_slice.argmax(axis=0) +# four_slice[max_values_indx, :] = np.nan + # out_slice = np.reshape(four_slice, (shaped_diffs.shape)) + # fits.writeto("outslice.fits", out_slice, overwrite=True) + # location = np.reshape(four_slice.argmax(axis=0), (nrows, ncols)) + # max_values = np.reshape(location, (len(row4), len(col4))) + # four_slice[max_values] = np.nan # mask largest group in slice + # med = np.nanmedian(four_slice, axis=0) + # hold = np.unravel_index(np.nanmedian(four_slice, axis=(0, 1)), (num_usable_diffs.shape[1], num_usable_diffs.shape[2])) + # median_diffs[row4, col4] = np.nanmedian(four_slice, axis=0) # add median to return arr for these pix + + # process groups with 3 usable groups + row3, col3 = np.where(num_usable_diffs == 3) # locations of == 3 usable diff pixels + if len(row3) > 0: + three_slice = shaped_diffs[:, row3, col3] + median_diffs[row3, col3] = np.nanmedian(three_slice, axis=0) # add median to return arr for these pix + + # process groups with 2 usable groups + row2, col2 = np.where(num_usable_diffs == 2) # locations of == 2 usable diff pixels + if len(row2) > 0: + two_slice = shaped_diffs[ :, row2, col2] + two_slice[np.nanargmax(np.abs(two_slice), axis=0), + np.arange(two_slice.shape[1])] = np.nan # mask larger abs. val + median_diffs[row2, col2] = np.nanmin(two_slice, axis=0) # add med. to return arr + + # set the medians all groups with less than 2 usable diffs to nan to skip further + # calculations for these pixels + row_none, col_none = np.where(num_usable_diffs < 2) + median_diffs[row_none, col_none] = np.nan + + return median_diffs diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index d5fccc0f..afe106f9 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -21,6 +21,23 @@ def _cube(ngroups, nints=1, nrows=204, ncols=204, readnoise=10): return _cube + +def test_varying_groups(setup_cube): + ngroups = 5 + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2, nrows=2, ncols=2, readnoise=8) + data[0, :, 0, 0] = [10, 20, 30, 530, 540] + data[0, :, 0, 1] = [10, 20, 30, 530, np.nan] + data[0, :, 1, 0] = [10, 20, 530, np.nan, np.nan] + data[0, :, 1, 1] = [10, 520, np.nan, np.nan, np.nan] + out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( + data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS + ) + assert np.array_equal(out_gdq[0, :, 0, 0], [0, 0, 0, 4, 0]) + assert np.array_equal(out_gdq[0, :, 0, 1], [0, 0, 0, 4, 0]) + assert np.array_equal(out_gdq[0, :, 1, 0], [0, 0, 4, 0, 0]) + assert np.array_equal(out_gdq[0, :, 1, 1], [0, 4, 0, 0, 0]) + + def test_multint_pixel(setup_cube): ngroups=4 data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=7, nrows=2, ncols=2, readnoise=8) @@ -168,7 +185,10 @@ def test_6grps_cr2_nframe2(setup_cube): data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) assert np.max(out_gdq) == 4 # a CR was found - assert(np.array_equal([0, 4, 4, 0, 0, 0], out_gdq[0, :, 1, 1])) + assert (np.array_equal([0, 4, 4, 0, 0, 0], out_gdq[0, :, 1, 1])) + assert (np.max(out_gdq[0, :, 0, 0]) == 0) + assert (np.max(out_gdq[0, :, 1, 0]) == 0) + assert (np.max(out_gdq[0, :, 0, 1]) == 0) def test_4grps_twocrs_2nd_4th(setup_cube): @@ -629,7 +649,7 @@ def test_5grps_satat4_crat3(setup_cube): # assert(4 == np.max(out_gdq)) # no CR was found result = out_gdq[0, :, 1, 1] assert np.array_equal( - [0, DQFLAGS['JUMP_DET'], DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], + [0, 0, DQFLAGS['JUMP_DET'], DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], out_gdq[0, :, 1, 1]) assert np.array_equal( [0, 0, 0, DQFLAGS['SATURATED'], DQFLAGS['SATURATED']], @@ -1042,15 +1062,17 @@ def test_median_func(): arr = np.array([1.0, 2.0, 3.0, 4.0, 5]) assert calc_med_first_diffs(arr) == 2.5 # 3d array, no nans - arr = np.zeros(5 * 2 * 2).reshape(5, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0, 4.0, 5]) - assert calc_med_first_diffs(arr)[0, 0] == 2.5 + arr = np.zeros(5 * 2 * 2).reshape(1, 5, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0, 5.0, 4]) + arr[0, 1, 0, 1] = 1.0 + result = calc_med_first_diffs(arr) + assert result[0, 0] == 2.5 # 1d, with nans arr = np.array([1.0, 2.0, 3.0, np.nan, 4.0, 5, np.nan]) assert calc_med_first_diffs(arr) == 2.5 # 3d, with nans - arr = np.zeros(7 * 2 * 2).reshape(7, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, 4.0, 5, np.nan]) + arr = np.zeros(7 * 2 * 2).reshape(1, 7, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, 4.0, 5, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == 2.5 # single pix with exactly 4 good diffs, should also clip 1 pix and return median @@ -1058,45 +1080,45 @@ def test_median_func(): arr = np.array([1.0, 2.0, 3.0, 4.0]) assert calc_med_first_diffs(arr) == 2 # 3d array, no nans - arr = np.zeros(4 * 2 * 2).reshape(4, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0, 4.0]) + arr = np.zeros(4 * 2 * 2).reshape(1, 4, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0, 4.0]) assert calc_med_first_diffs(arr)[0, 0] == 2 # 1d, with nans arr = np.array([1.0, 2.0, 3.0, np.nan, 4.0, np.nan]) assert calc_med_first_diffs(arr) == 2 # 3d, with nans - arr = np.zeros(6 * 2 * 2).reshape(6, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, 4.0, np.nan]) + arr = np.zeros(6 * 2 * 2).reshape(1, 6, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, 4.0, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == 2 # single pix with exactly 3 good diffs, should compute median without clipping arr = np.array([1.0, 2.0, 3.0]) assert calc_med_first_diffs(arr) == 2 # 3d array, no nans - arr = np.zeros(3 * 2 * 2).reshape(3, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0]) + arr = np.zeros(3 * 2 * 2).reshape(1, 3, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0]) assert calc_med_first_diffs(arr)[0, 0] == 2 # 1d, with nans arr = np.array([1.0, 2.0, 3.0, np.nan, np.nan]) assert calc_med_first_diffs(arr) == 2 # 3d, with nans - arr = np.zeros(5 * 2 * 2).reshape(5, 2, 2) - arr[:, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, np.nan]) + arr = np.zeros(5 * 2 * 2).reshape(1, 5, 2, 2) + arr[0, :, 0, 0] = np.array([1.0, 2.0, 3.0, np.nan, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == 2 # # single pix with exactly 2 good diffs, should return the element with the minimum abs val arr = np.array([-1.0, -2.0]) assert calc_med_first_diffs(arr) == -1 # 3d array, no nans - arr = np.zeros(2 * 2 * 2).reshape(2, 2, 2) - arr[:, 0, 0] = np.array([-1.0, -2.0]) + arr = np.zeros(2 * 2 * 2).reshape(1, 2, 2, 2) + arr[0, :, 0, 0] = np.array([-1.0, -2.0]) assert calc_med_first_diffs(arr)[0, 0] == -1 # 1d, with nans arr = np.array([-1.0, -2.0, np.nan, np.nan]) assert calc_med_first_diffs(arr) == -1 # 3d, with nans - arr = np.zeros(4 * 2 * 2).reshape(4, 2, 2) - arr[:, 0, 0] = np.array([-1.0, -2.0, np.nan, np.nan]) + arr = np.zeros(4 * 2 * 2).reshape(1, 4, 2, 2) + arr[0, :, 0, 0] = np.array([-1.0, -2.0, np.nan, np.nan]) assert calc_med_first_diffs(arr)[0, 0] == -1 @pytest.mark.skip("Used for local testing") def test_sigma_clip(): From 798c864252aa5859e6d39bf673877a6ad50c2fe7 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Feb 2024 13:17:26 -0500 Subject: [PATCH 174/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index afe106f9..acc3c6d4 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -24,7 +24,7 @@ def _cube(ngroups, nints=1, nrows=204, ncols=204, readnoise=10): def test_varying_groups(setup_cube): ngroups = 5 - data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=2, nrows=2, ncols=2, readnoise=8) + data, gdq, nframes, read_noise, rej_threshold = setup_cube(ngroups, nints=1, nrows=2, ncols=2, readnoise=8) data[0, :, 0, 0] = [10, 20, 30, 530, 540] data[0, :, 0, 1] = [10, 20, 30, 530, np.nan] data[0, :, 1, 0] = [10, 20, 530, np.nan, np.nan] @@ -35,7 +35,7 @@ def test_varying_groups(setup_cube): assert np.array_equal(out_gdq[0, :, 0, 0], [0, 0, 0, 4, 0]) assert np.array_equal(out_gdq[0, :, 0, 1], [0, 0, 0, 4, 0]) assert np.array_equal(out_gdq[0, :, 1, 0], [0, 0, 4, 0, 0]) - assert np.array_equal(out_gdq[0, :, 1, 1], [0, 4, 0, 0, 0]) + assert np.array_equal(out_gdq[0, :, 1, 1], [0, 0, 0, 0, 0]) def test_multint_pixel(setup_cube): From 9fd862818368b5537c45312553b301b84b2ceb97 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Feb 2024 13:32:46 -0500 Subject: [PATCH 175/196] Cleanup --- src/stcal/jump/twopoint_difference.py | 47 +++------------------------ 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index fc454d76..041af781 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -5,8 +5,6 @@ import warnings from astropy import stats -from astropy.io import fits - log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -114,6 +112,10 @@ def find_crs( integrations. This means that a group will only be compared against the same group in other integrations. If False all groups across all integrations will be used to detect outliers. + + min_grps_single_pass: integer + The minimum number of groups to switch from the iterative flagging of + cosmic rays to just finding all the outliers at once. Returns ------- gdq : int, 4D array @@ -252,10 +254,8 @@ def find_crs( ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] masked_ratio = np.ma.masked_greater(ratio, normal_rej_thresh) + # The jump mask is the ratio greater than the threshold and the difference is usable jump_mask = np.logical_and(masked_ratio.mask, np.logical_not(first_diffs_masked.mask)) -# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == sat_flag)] = False -# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == dnu_flag)] = False -# jump_mask[np.bitwise_and(jump_mask, gdq[:, 1:, :, :] == (dnu_flag + sat_flag))] = False gdq[:, 1:, :, :] = np.bitwise_or(gdq[:, 1:, :, :], jump_mask * np.uint8(dqflags["JUMP_DET"])) else: # low number of diffs requires iterative flagging @@ -306,7 +306,6 @@ def find_crs( # repeat this process until no more CRs are found. for j in range(len(all_crs_row)): # get arrays of abs(diffs), ratio, readnoise for this pixel - TEMP_first_DIFFS = first_diffs[:, :, all_crs_row[j], all_crs_col[j]] pix_first_diffs = first_diffs[:, :, all_crs_row[j], all_crs_col[j]] pix_ratio = ratio[:, :, all_crs_row[j], all_crs_col[j]] pix_rn2 = read_noise_2[all_crs_row[j], all_crs_col[j]] @@ -315,18 +314,7 @@ def find_crs( pix_cr_mask = np.ones(pix_first_diffs.shape, dtype=bool) # set the largest ratio as a CR - test = np.nanargmax(pix_ratio) - int = (test )//ngrps - grp = (test ) - int * ngrps - shape = test.shape - print("test size", test.shape) - pix_ratio_tmp = np.nanargmax(pix_ratio) location = np.unravel_index(np.nanargmax(pix_ratio), pix_ratio.shape) -# if test.shape == (1, 1): -# index = np.zeros((1, 1), dtype=np.int32) -# index[0, 0] = np.nanargmax(pix_ratio) - - # pix_cr_mask[test] = 0 pix_cr_mask[location] = 0 new_CR_found = True @@ -357,8 +345,6 @@ def find_crs( rej_thresh = two_diff_rej_thresh max_idx = np.nanargmax(new_pix_ratio) location = np.unravel_index(max_idx, new_pix_ratio.shape) - # mask[location] = False - # new_pix_max_ratio_idx = np.nanargmax(new_pix_ratio) # index of largest ratio if new_pix_ratio[location] > rej_thresh: new_CR_found = True pix_cr_mask[location] = 0 @@ -368,7 +354,6 @@ def find_crs( gdq[:, 1:, all_crs_row[j], all_crs_col[j]], dqflags["JUMP_DET"] * np.invert(pix_cr_mask), ) - fits.writeto("outgdq.fits", gdq, overwrite=True) cr_integ, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) num_primary_crs = len(cr_group) if flag_4_neighbors: # iterate over each 'jump' pixel @@ -532,32 +517,10 @@ def calc_med_first_diffs(in_first_diffs): # process groups with >=4 usable diffs row4, col4 = np.where(num_usable_diffs >= 4) # locations of >= 4 usable diffs pixels if len(row4) > 0: -# mask = np.ones_like(shaped_diffs).astype(bool) -# location = np.unravel_index(shaped_diffs.argmax(), shaped_diffs.shape) -# mask[location] = False # clip the diff with the largest abs value -# masked_first_diffs = shaped_diffs[mask] -# result = np.nanmedian(first_diffs[mask], axis=(0, 1)) -# return np.nanmedian(first_diffs[mask], axis=(0, 1)) - four_slice = shaped_diffs[:, row4, col4] loc0 = np.nanargmax(four_slice, axis=0) - tmp1 = np.argmax(shaped_diffs, axis=1) - tmp2 = np.argmax(shaped_diffs, axis=2) - location = np.unravel_index(np.argmax(four_slice, axis=0), (four_slice.shape)) shaped_diffs[loc0, row4, col4] = np.nan median_diffs[row4, col4] = np.nanmedian(shaped_diffs[:, row4, col4], axis=0) -# return np.nanmedian(four_slice[:, row4, col4]) -# tmp = four_slice.argmax(axis=0) -# max_values_indx = four_slice.argmax(axis=0) -# four_slice[max_values_indx, :] = np.nan - # out_slice = np.reshape(four_slice, (shaped_diffs.shape)) - # fits.writeto("outslice.fits", out_slice, overwrite=True) - # location = np.reshape(four_slice.argmax(axis=0), (nrows, ncols)) - # max_values = np.reshape(location, (len(row4), len(col4))) - # four_slice[max_values] = np.nan # mask largest group in slice - # med = np.nanmedian(four_slice, axis=0) - # hold = np.unravel_index(np.nanmedian(four_slice, axis=(0, 1)), (num_usable_diffs.shape[1], num_usable_diffs.shape[2])) - # median_diffs[row4, col4] = np.nanmedian(four_slice, axis=0) # add median to return arr for these pix # process groups with 3 usable groups row3, col3 = np.where(num_usable_diffs == 3) # locations of == 3 usable diff pixels From 0139f919345d8d11a1bd266dfff2589f1628cc8e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Feb 2024 13:35:20 -0500 Subject: [PATCH 176/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index acc3c6d4..b17aee30 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -94,8 +94,6 @@ def test_4grps_2ints_cr2_noflux(setup_cube): data, gdq, read_noise, rej_threshold, rej_threshold, rej_threshold, nframes, False, 200, 10, DQFLAGS ) assert np.max(out_gdq) == 4 # a CR was found - fits.writeto("outgdq_final.fits", out_gdq, overwrite=True) - gdq_11 = out_gdq[:, :, 1, 1] assert np.argmax(out_gdq[1, :, 1, 1]) == 1 # find the CR in the expected group assert(1 == np.argmax(out_gdq[1, :, 1, 1])) # find the CR in the expected group @@ -1122,7 +1120,6 @@ def test_median_func(): assert calc_med_first_diffs(arr)[0, 0] == -1 @pytest.mark.skip("Used for local testing") def test_sigma_clip(): -# hdul = fits.open('TSOjump_sc__refpix.fits') hdul = fits.open('lrs_TSOjump_sigmaclip5_00_refpix.fits') data = hdul['SCI'].data * 4.0 gdq = hdul['GROUPDQ'].data From 96bc9e254bcb84190c7a6dfe93a1d61ce0e09f3a Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Feb 2024 15:42:51 -0500 Subject: [PATCH 177/196] Update CHANGES.rst --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 164b90ee..2d3ea87c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,12 @@ 1.5.3 (unreleased) ================== -- +-Jump +------------ +- Enable the use of multiple integrations to find outliers. Also, + when the number of groups is above a threshold use single pass + outlier flagging rather than the iterative flagging. + 1.5.2 (2023-12-13) ================== From c892def2261d40e111d185de72b57d41e2d5e21c Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 21 Feb 2024 16:49:38 -0500 Subject: [PATCH 178/196] Update CHANGES.rst --- CHANGES.rst | 58 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2d3ea87c..64a43234 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,49 @@ -1.5.3 (unreleased) +1.6.1 (unreleased) ================== --Jump ------------- + + +1.6.0 (2024-02-15) +================== + +Changes to API +-------------- + +jump +~~~~ + +- Add in the flagging of groups in the integration after a snowball + occurs. The saturated core of the snowball gets flagged as jump + for a number of groups passed in as a parameter [#238] + +Bug Fixes +--------- + +jump +~~~~ + +- Fixed the computation of the number of rows per slice for multiprocessing, which + was causing different results when running the step with multiprocess [#239] + +- Fix the code to at least always flag the group with the shower and the requested + groups after the primary shower. [#237] + - Enable the use of multiple integrations to find outliers. Also, when the number of groups is above a threshold use single pass - outlier flagging rather than the iterative flagging. + outlier flagging rather than the iterative flagging. [#242] + +Other +----- + +jump +~~~~ + +- Reorganize jump docs between the jwst and stcal repos. [#240] + +ramp_fitting +~~~~~~~~~~~~ + +- Reorganize ramp_fitting docs between the jwst and stcal repos. [#240] 1.5.2 (2023-12-13) @@ -29,7 +67,7 @@ Other - Enable automatic linting and code style checks [#187] ramp_fitting ------------- +~~~~~~~~~~~~ - Refactor Casertano, et.al, 2022 uneven ramp fitting and incorporate the matching jump detection algorithm into it. [#215] @@ -89,13 +127,16 @@ jump within a group. [#207] - Added more allowable selections for the number of cores to use for - multiprocessing [#183]. + multiprocessing [#183] + +- Fixed the computation of the number of rows per slice for multiprocessing, + which caused different results when running the step with multiprocess [#239] ramp_fitting ~~~~~~~~~~~~ - Added more allowable selections for the number of cores to use for - multiprocessing [#183]. + multiprocessing [#183] - Updating variance computation for invalid integrations, as well as updating the median rate computation by excluding groups marked as @@ -144,7 +185,6 @@ jump Bug Fixes --------- ->>>>>>> 75e1efacba98506604174702b747e7586e9fbc95 jump ~~~~ @@ -810,4 +850,4 @@ ramp_fitting 0.1.0 (2021-03-19) ================== -- Added code to manipulate bitmasks. +- Added code to manipulate bitmasks. \ No newline at end of file From 025216929b8090fa2c79246420494884541e24bb Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Thu, 22 Feb 2024 13:27:48 -0500 Subject: [PATCH 179/196] update variable name and parameter docs --- src/stcal/jump/jump.py | 28 +++++++++++++++++++-------- src/stcal/jump/twopoint_difference.py | 6 +++--- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 92b35d7e..984fe35d 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -54,7 +54,7 @@ def detect_jumps( minimum_groups=3, minimum_sigclip_groups=100, only_use_ints=True, - min_grps_single_pass=10, + min_diffs_single_pass=10, ): """ This is the high-level controlling routine for the jump detection process. @@ -206,6 +206,15 @@ def detect_jumps( min_sat_radius_extend : float The minimum radius of the saturated core of a snowball for the core to be extended + minimum_groups: int + The minimum number of groups for jump detection + minimum_sigclip_groups: int + The minimum number of groups required to use sigma clipping to find outliers. + only_use_ints: boolean + In sigma clipping, if True only differences between integrations are compared. If False, + then all differences are processed at once. + min_diffs_single_pass: int + The minimum number of groups to switch to flagging all outliers in a single pass. Returns ------- @@ -277,7 +286,7 @@ def detect_jumps( minimum_groups=3, minimum_sigclip_groups=minimum_sigclip_groups, only_use_ints=only_use_ints, - minimum_primary_crs=min_grps_single_pass, + min_diffs_single_pass=min_diffs_single_pass, ) # This is the flag that controls the flagging of snowballs. if expand_large_events: @@ -353,7 +362,7 @@ def detect_jumps( minimum_groups, minimum_sigclip_groups, only_use_ints, - min_grps_single_pass, + min_diffs_single_pass, ), ) @@ -380,7 +389,7 @@ def detect_jumps( minimum_groups, minimum_sigclip_groups, only_use_ints, - min_grps_single_pass, + min_diffs_single_pass, ), ) log.info("Creating %d processes for jump detection ", n_slices) @@ -827,10 +836,13 @@ def find_faint_extended( ellipse_expand: float The relative increase in the size of the fitted ellipse to be applied to the shower. - num_grps_masked: int - The number of groups after the detected shower to be flagged as jump. - max_extended_radius: int - The upper limit for the extension of saturation and jump + num_grps_masked: int + The number of groups after the detected shower to be flagged as jump. + max_extended_radius: int + The upper limit for the extension of saturation and jump + minimum_sigclip_groups: int + The minimum number of groups to use sigma clipping. + Returns ------- diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 041af781..20f248e0 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -29,7 +29,7 @@ def find_crs( minimum_groups=3, minimum_sigclip_groups=100, only_use_ints=True, - min_grps_single_pass=10, + min_diffs_single_pass=10, ): """ Find CRs/Jumps in each integration within the input data array. The input @@ -113,7 +113,7 @@ def find_crs( same group in other integrations. If False all groups across all integrations will be used to detect outliers. - min_grps_single_pass: integer + min_diffs_single_pass: integer The minimum number of groups to switch from the iterative flagging of cosmic rays to just finding all the outliers at once. Returns @@ -240,7 +240,7 @@ def find_crs( # use mask on data, so the results will have sat/donotuse groups masked first_diffs = np.diff(dat, axis=1) - if total_usable_diffs >= min_grps_single_pass: + if total_usable_diffs >= min_diffs_single_pass: median_diffs = np.nanmedian(first_diffs, axis=(0, 1)) # calculate sigma for each pixel sigma = np.sqrt(np.abs(median_diffs) + read_noise_2 / nframes) From 3f383fc393e710dee0c14e9d269ec5da74d0593f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 11:30:02 -0500 Subject: [PATCH 180/196] Update .gitignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index b41dbe6f..d35d7af2 100644 --- a/.gitignore +++ b/.gitignore @@ -148,11 +148,7 @@ src/stcal/_version.py # auto-generated API docs docs/source/api -<<<<<<< HEAD -.DS_Store -======= .DS_Store # VSCode stuff .vscode ->>>>>>> 75e1efacba98506604174702b747e7586e9fbc95 From c6762f687b2b47d703658d22103bbfc6f15a60cf Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 11:32:34 -0500 Subject: [PATCH 181/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index 46d68c49..bff39245 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -164,9 +164,9 @@ def find_crs( total_groups = nints * (ngrps - num_flagged_grps) total_diffs = nints * (ngrps - 1 - num_flagged_grps) total_usable_diffs = total_diffs - num_flagged_grps - if (ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or \ + if ((ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or (not only_use_ints and nints * ngrps < minimum_sigclip_groups and - total_groups < minimum_groups): + total_groups < minimum_groups)): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") log.info("Data shape {}".format(str(dat.shape))) dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), From 5df2397cec8c0f79897c7751b2d2e993a780106d Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 12:56:16 -0500 Subject: [PATCH 182/196] Update twopoint_difference.py --- src/stcal/jump/twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index bff39245..bed96529 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -164,7 +164,7 @@ def find_crs( total_groups = nints * (ngrps - num_flagged_grps) total_diffs = nints * (ngrps - 1 - num_flagged_grps) total_usable_diffs = total_diffs - num_flagged_grps - if ((ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or + if ((ngrps < minimum_groups and only_use_ints and nints < minimum_sigclip_groups) or (not only_use_ints and nints * ngrps < minimum_sigclip_groups and total_groups < minimum_groups)): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") From d2f98de289382bde55d4bb6af3dbab8ec16b9235 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 13:20:11 -0500 Subject: [PATCH 183/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 5cbd18fc..2c4f1b16 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -906,7 +906,7 @@ def test_10grps_1cr_afterjump(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = 1.0 + after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, From 32341a43c946b31988b3456a341e07461dcdb6d1 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 13:23:32 -0500 Subject: [PATCH 184/196] clean up flag after --- src/stcal/jump/twopoint_difference.py | 2 +- tests/test_twopoint_difference.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index bed96529..e7452ee7 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -424,7 +424,7 @@ def find_crs( flag_e_threshold = [after_jump_flag_e1, after_jump_flag_e2] flag_groups = [after_jump_flag_n1, after_jump_flag_n2] for cthres, cgroup in zip(flag_e_threshold, flag_groups): - if cgroup > 0 and cthres > 0: + if cgroup > 0: cr_intg, cr_group, cr_row, cr_col = np.where(np.bitwise_and(gdq, jump_flag)) for j in range(len(cr_group)): intg = cr_intg[j] diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 2c4f1b16..134cf112 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1019,8 +1019,8 @@ def test_10grps_1cr_afterjump_twothresholds(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = 500.0 - after_jump_flag_e2 = 10.0 + after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 500.0 + after_jump_flag_e2 = np.full(data.shape[2:4], 1.0) * 10.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, From 289c3fa56acea5d64407dc3a7b848486a1f09031 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 13:28:59 -0500 Subject: [PATCH 185/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 134cf112..dc85a301 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -942,7 +942,7 @@ def test_10grps_1cr_afterjump_2group(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = 1.0 + after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, @@ -983,7 +983,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = 10000.0 + after_jump_flag_e1 = np.full(data.shape[2:4], 10000.0) * 0.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, From a0783a9b0b6e0b7f233c54c3c236e43c751617db Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Mon, 26 Feb 2024 13:31:01 -0500 Subject: [PATCH 186/196] fix merge problems --- tests/test_twopoint_difference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index dc85a301..b17aee30 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -983,7 +983,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = np.full(data.shape[2:4], 10000.0) * 0.0 + after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 10000.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, From d292bb5c92bf68df3f87367665491ae103dfa546 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 27 Feb 2024 13:21:44 -0500 Subject: [PATCH 187/196] Update jump.py --- src/stcal/jump/jump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 51bb100b..12969a55 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -251,7 +251,7 @@ def detect_jumps( # also apply to the after_jump thresholds after_jump_flag_e1 = after_jump_flag_dn1 * np.nanmedian(gain_2d) after_jump_flag_e2 = after_jump_flag_dn2 * np.nanmedian(gain_2d) - + print("after_jump_flag_e1: shape", after_jump_flag_e1.shape) # Apply the 2-point difference method as a first pass log.info("Executing two-point difference method") start = time.time() From cf098f5959d5740bd7db79148b6c12e910975d7e Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 28 Feb 2024 13:24:07 -0500 Subject: [PATCH 188/196] add test from JWST --- tests/test_jump.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/test_jump.py b/tests/test_jump.py index 04a00ee2..f775b320 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -31,7 +31,52 @@ def _cube(ngroups, readnoise=10): return _cube - +def test_nirspec_saturated_pix(): + """ + This test is based on an actual NIRSpec exposure that has some pixels + flagged as saturated in one or more groups, which the jump step is + supposed to ignore, but an old version of the code was setting JUMP flags + for some of the saturated groups. This is to verify that the saturated + groups are no longer flagged with jumps. + """ + ingain = 1.0 + inreadnoise = 10.7 + ngroups = 7 + nrows = 2 + ncols = 2 + nints = 1 + nframes = 1 + data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + read_noise = np.full((nrows, ncols), inreadnoise, dtype=np.float32) + gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint32) + err = np.zeros(shape=(nrows, ncols), dtype=np.float32) + pdq = np.zeros(shape=(nrows, ncols), dtype=np.uint32) + gain = np.ones_like(read_noise) * ingain + + # Setup the needed input pixel and DQ values + data[0, :, 1, 1] = [639854.75, 4872.451, -17861.791, 14022.15, 22320.176, + 1116.3828, 1936.9746] + gdq[0, :, 1, 1] = [0, 0, 0, 0, 0, 2, 2] + data[0, :, 0, 1] = [8.25666812e+05, -1.10471914e+05, 1.95755371e+02, 1.83118457e+03, + 1.72250879e+03, 1.81733496e+03, 1.65188281e+03] + # 2 non-sat groups means only 1 non-sat diff, so no jumps should be flagged + gdq[0, :, 0, 1] = [0, 0, 2, 2, 2, 2, 2] + data[0, :, 1, 0] = [1228767., 46392.234, -3245.6553, 7762.413, + 37190.76, 266611.62, 5072.4434] + gdq[0, :, 1, 0] = [0, 0, 0, 0, 0, 0, 2] + + # run jump detection + gdq, pdq, total_primary_crs, number_extended_events, stddev = detect_jumps(nframes, data, gdq, pdq, err, gain, read_noise, rejection_thresh=4.0, three_grp_thresh=5, + four_grp_thresh=6, + max_cores='none', max_jump_to_flag_neighbors=200, + min_jump_to_flag_neighbors=10, flag_4_neighbors=True, dqflags=DQFLAGS) + + # Check the results. There should not be any pixels with DQ values of 6, which + # is saturated (2) plus jump (4). All the DQ's should be either just 2 or just 4. + np.testing.assert_array_equal(gdq[0, :, 1, 1], [0, 4, 0, 4, 4, 2, 2]) + # assert that no groups are flagged when theres only 1 non-sat. grp + np.testing.assert_array_equal(gdq[0, :, 0, 1], [0, 0, 2, 2, 2, 2, 2]) + np.testing.assert_array_equal(gdq[0, :, 1, 0], [0, 4, 4, 0, 4, 4, 2]) def test_multiprocessing(): nints = 1 nrows = 13 From 0e94cb89a39c261ffd85c6849f56918f488dd1aa Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 28 Feb 2024 16:01:22 -0500 Subject: [PATCH 189/196] change threshold flagging back to scalar --- src/stcal/jump/twopoint_difference.py | 2 +- tests/test_twopoint_difference.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index e7452ee7..b161c4dc 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -431,7 +431,7 @@ def find_crs( group = cr_group[j] row = cr_row[j] col = cr_col[j] - if e_jump_4d[intg, group - 1, row, col] >= cthres[row, col]: + if e_jump_4d[intg, group - 1, row, col] >= cthres: for kk in range(group, min(group + cgroup + 1, ngroups)): if (gdq[intg, kk, row, col] & sat_flag) == 0 and ( gdq[intg, kk, row, col] & dnu_flag diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index b17aee30..22ff41a6 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -906,7 +906,7 @@ def test_10grps_1cr_afterjump(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 + after_jump_flag_e1 = 0.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, @@ -942,7 +942,7 @@ def test_10grps_1cr_afterjump_2group(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 0.0 + after_jump_flag_e1 = 0.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, @@ -983,7 +983,7 @@ def test_10grps_1cr_afterjump_toosmall(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 10000.0 + after_jump_flag_e1 = 10000.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, @@ -1019,8 +1019,8 @@ def test_10grps_1cr_afterjump_twothresholds(setup_cube): data[0, 8, 100, 100] = 1190 data[0, 9, 100, 100] = 1209 - after_jump_flag_e1 = np.full(data.shape[2:4], 1.0) * 500.0 - after_jump_flag_e2 = np.full(data.shape[2:4], 1.0) * 10.0 + after_jump_flag_e1 = 500.0 + after_jump_flag_e2 = 10.0 out_gdq, row_below_gdq, rows_above_gdq, total_crs, stddev = find_crs( data, gdq, From 6c105d762fb0cee2556fb5966294569c359cfad2 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Wed, 28 Feb 2024 17:29:08 -0500 Subject: [PATCH 190/196] make sure copies are made of model attributes --- src/stcal/jump/jump.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index 12969a55..249278a2 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -18,10 +18,10 @@ def detect_jumps( frames_per_group, - data, + indata, gdq, pdq, - err, + inerr, gain_2d, readnoise_2d, rejection_thresh, @@ -82,7 +82,7 @@ def detect_jumps( frames_per_group : int number of frames per group - data : float, 4D array + indata : float, 4D array science array gdq : int, 4D array @@ -91,7 +91,7 @@ def detect_jumps( pdq : int, 2D array pixelg dq array - err : float, 4D array + inerr : float, 4D array error array gain_2d : float, 2D array @@ -245,8 +245,8 @@ def detect_jumps( # Apply gain to the SCI, ERR, and readnoise arrays so they're in units # of electrons - data *= gain_2d - err *= gain_2d + data = indata * gain_2d + err = inerr * gain_2d readnoise_2d *= gain_2d # also apply to the after_jump thresholds after_jump_flag_e1 = after_jump_flag_dn1 * np.nanmedian(gain_2d) From 54a1240793abadf81cf78c3cf23dc97f1bdb13d6 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 1 Mar 2024 10:08:37 -0500 Subject: [PATCH 191/196] Update test_jump.py --- tests/test_jump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_jump.py b/tests/test_jump.py index f775b320..469164b6 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -77,6 +77,7 @@ def test_nirspec_saturated_pix(): # assert that no groups are flagged when theres only 1 non-sat. grp np.testing.assert_array_equal(gdq[0, :, 0, 1], [0, 0, 2, 2, 2, 2, 2]) np.testing.assert_array_equal(gdq[0, :, 1, 0], [0, 4, 4, 0, 4, 4, 2]) + def test_multiprocessing(): nints = 1 nrows = 13 From 9dcfe1885ee7c626e33ef9d89a35b38c482331f1 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 1 Mar 2024 10:12:40 -0500 Subject: [PATCH 192/196] Update test_jump.py --- tests/test_jump.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_jump.py b/tests/test_jump.py index 469164b6..556d554d 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -74,7 +74,7 @@ def test_nirspec_saturated_pix(): # Check the results. There should not be any pixels with DQ values of 6, which # is saturated (2) plus jump (4). All the DQ's should be either just 2 or just 4. np.testing.assert_array_equal(gdq[0, :, 1, 1], [0, 4, 0, 4, 4, 2, 2]) - # assert that no groups are flagged when theres only 1 non-sat. grp + # assert that no groups are flagged when there's only 1 non-sat. grp np.testing.assert_array_equal(gdq[0, :, 0, 1], [0, 0, 2, 2, 2, 2, 2]) np.testing.assert_array_equal(gdq[0, :, 1, 0], [0, 4, 4, 0, 4, 4, 2]) @@ -170,7 +170,6 @@ def test_multiprocessing_big(): assert gdq[0, 4, 204, 6] == DQFLAGS['DO_NOT_USE'] #This value would have been 5 without the fix. - def test_find_simple_ellipse(): plane = np.zeros(shape=(5, 5), dtype=np.uint8) plane[2, 2] = DQFLAGS["JUMP_DET"] @@ -531,7 +530,7 @@ def test_flag_persist_groups(): sat_expand=1.1, mask_persist_grps_next_int=True, persist_grps_flagged=0) -# fits.writeto("persitflaggedgdq.fits", gdq, overwrite=True) + def test_calc_num_slices(): n_rows = 20 max_available_cores = 10 From 9518003a7c9a50dc5aa1a40cfa10d672658d22ce Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 1 Mar 2024 10:13:12 -0500 Subject: [PATCH 193/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 22ff41a6..4350b5bd 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1195,20 +1195,6 @@ def test_5grp_realTSO(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) -@pytest.mark.skip("Used for local testing") -def test_5grp_allTSO(): - hdul = fits.open("obs2508_noshower_sigclip_base_00_dark_current.fits") - gdq = hdul['groupdq'].data - data = hdul['sci'].data * 5.5 - readnoise = 5.5 * 6 - read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) - - gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ - find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, - after_jump_flag_e1=0.0, after_jump_flag_n1=0, - after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) - fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) @pytest.mark.skip("Used for local testing") def test_1059(): From 32687ff73d257fd4eddfeef5655c1902abd13c4f Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Fri, 1 Mar 2024 10:16:10 -0500 Subject: [PATCH 194/196] Update test_twopoint_difference.py --- tests/test_twopoint_difference.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/test_twopoint_difference.py b/tests/test_twopoint_difference.py index 4350b5bd..ce6d7b14 100644 --- a/tests/test_twopoint_difference.py +++ b/tests/test_twopoint_difference.py @@ -1171,7 +1171,6 @@ def test_5grp_TSO(): np.expand_dims(gdq, axis=0) np.expand_dims(data, axis=0) gdq[:, 0, :, :] = DQFLAGS['DO_NOT_USE'] -# gdq[1:, 1, :, :] = DQFLAGS['DO_NOT_USE'] gdq[:, -1, :, :] = DQFLAGS['DO_NOT_USE'] data[0, :, 0, 0] = [21500, 37600, 52082, 65068, 58627] data[0, :, 0, 1] = [21500, 37600, 52082, 65068, 58627] @@ -1195,33 +1194,3 @@ def test_5grp_realTSO(): after_jump_flag_e2=0.0, after_jump_flag_n2=0, copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) fits.writeto("new_gdq_cutout.fits", gdq, overwrite=True) - -@pytest.mark.skip("Used for local testing") -def test_1059(): - hdul = fits.open("data/nircam_1059_00_dark_current.fits") - gdq = hdul['groupdq'].data - data = hdul['sci'].data * 5.5 - readnoise = 5.5 * 6 - read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) - - gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ - find_crs(data, gdq, read_noise, 5, 4, 5, 1, False, 1000, 10, DQFLAGS, - after_jump_flag_e1=0.0, after_jump_flag_n1=0, - after_jump_flag_e2=0.0, after_jump_flag_n2=0, - copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=15000) - fits.writeto("new_no_sigma_clip_gdq.fits", gdq, overwrite=True) - -@pytest.mark.skip("Used for local testing") -def test_1952(): - hdul = fits.open("data/obs1952_sc_shower_00_dark_current.fits") - gdq = hdul['groupdq'].data - data = hdul['sci'].data * 5.5 - readnoise = 5.5 * 6 - read_noise = np.full((gdq.shape[2], gdq.shape[3]), readnoise, dtype=np.float32) - gain = np.full((gdq.shape[2], gdq.shape[3]), 5.5, dtype=np.float32) - gdq, row_below_gdq, row_above_gdq, total_total_crs, stddev = \ - find_crs(data, gdq, read_noise, 5, 4, 5, 1, True, 1000, 10, DQFLAGS, - after_jump_flag_e1=100.0 * gain, after_jump_flag_n1=20/2.77, - after_jump_flag_e2=3000.0 * gain, after_jump_flag_n2=1000/2.77, - copy_arrs=True, minimum_groups=3, minimum_sigclip_groups=150) - fits.writeto("new_obs1952_gdq.fits", gdq, overwrite=True) From 52744593453031dc97129e299bbcddc3d5c5a5fc Mon Sep 17 00:00:00 2001 From: Howard Bushouse Date: Mon, 18 Mar 2024 08:39:16 -0400 Subject: [PATCH 195/196] Update CHANGES.rst --- CHANGES.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7e6082ca..ba7c8756 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,14 @@ Bug Fixes Other ----- +jump +~~~~ + +- Enable the use of multiple integrations to find outliers. Also, + when the number of groups is above a threshold, use single pass + outlier flagging rather than the iterative flagging. [#242] + + 1.6.1 (2024-02-29) ================== @@ -46,12 +54,6 @@ jump - Fix the code to at least always flag the group with the shower and the requested groups after the primary shower. [#237] -- Enable the use of multiple integrations to find outliers. Also, - when the number of groups is above a threshold use single pass - outlier flagging rather than the iterative flagging. [#242] - -======= - Other ----- @@ -870,4 +872,4 @@ ramp_fitting 0.1.0 (2021-03-19) ================== -- Added code to manipulate bitmasks. \ No newline at end of file +- Added code to manipulate bitmasks. From 4b3de4be5192c7189e2ade99527b1a719bf36069 Mon Sep 17 00:00:00 2001 From: Mike Regan Date: Tue, 19 Mar 2024 13:42:47 -0400 Subject: [PATCH 196/196] addressed PR comments --- src/stcal/jump/jump.py | 27 +++++++++++++-------------- src/stcal/jump/twopoint_difference.py | 10 ++++------ tests/test_jump.py | 4 +++- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/stcal/jump/jump.py b/src/stcal/jump/jump.py index bc97952f..80017306 100644 --- a/src/stcal/jump/jump.py +++ b/src/stcal/jump/jump.py @@ -110,7 +110,7 @@ def detect_jumps( four_grp_thresh : float cosmic ray sigma rejection threshold for ramps having 4 groups - max_cores: str + max_cores : str Maximum number of cores to use for multiprocessing. Available choices are 'none' (which will create one process), 'quarter', 'half', 'all' (of available cpu cores). @@ -125,11 +125,11 @@ def detect_jumps( neighbors (marginal detections). Any primary jump below this value will not have its neighbors flagged. - flag_4_neighbors: bool + flag_4_neighbors : bool if set to True (default is True), it will cause the four perpendicular neighbors of all detected jumps to also be flagged as a jump. - dqflags: dict + dqflags : dict A dictionary with at least the following keywords: DO_NOT_USE, SATURATED, JUMP_DET, NO_GAIN_VALUE, GOOD @@ -210,14 +210,14 @@ def detect_jumps( min_sat_radius_extend : float The minimum radius of the saturated core of a snowball for the core to be extended - minimum_groups: int + minimum_groups : int The minimum number of groups for jump detection - minimum_sigclip_groups: int + minimum_sigclip_groups : int The minimum number of groups required to use sigma clipping to find outliers. - only_use_ints: boolean + only_use_ints : boolean In sigma clipping, if True only differences between integrations are compared. If False, then all differences are processed at once. - min_diffs_single_pass: int + min_diffs_single_pass : int The minimum number of groups to switch to flagging all outliers in a single pass. Returns @@ -251,7 +251,6 @@ def detect_jumps( # also apply to the after_jump thresholds after_jump_flag_e1 = after_jump_flag_dn1 * np.nanmedian(gain_2d) after_jump_flag_e2 = after_jump_flag_dn2 * np.nanmedian(gain_2d) - print("after_jump_flag_e1: shape", after_jump_flag_e1.shape) # Apply the 2-point difference method as a first pass log.info("Executing two-point difference method") start = time.time() @@ -573,7 +572,7 @@ def flag_large_events( Nothing, gdq array is modified. """ - log.info("Flagging large Snowballs") + log.info("Flagging Snowballs") n_showers_grp = [] total_snowballs = 0 @@ -908,7 +907,7 @@ def find_faint_extended( emission. min_shower_area : int The minimum area for a group of pixels to be flagged as a shower. - inner: int + inner : int The inner radius of the ring_2D_kernal used for the convolution. outer : int The outer radius of the ring_2D_kernal used for the convolution. @@ -916,14 +915,14 @@ def find_faint_extended( The integer value of the saturation flag. jump_flag : int The integer value of the jump flag - ellipse_expand: float + ellipse_expand : float The relative increase in the size of the fitted ellipse to be applied to the shower. - num_grps_masked: int + num_grps_masked : int The number of groups after the detected shower to be flagged as jump. - max_extended_radius: int + max_extended_radius : int The upper limit for the extension of saturation and jump - minimum_sigclip_groups: int + minimum_sigclip_groups : int The minimum number of groups to use sigma clipping. diff --git a/src/stcal/jump/twopoint_difference.py b/src/stcal/jump/twopoint_difference.py index b161c4dc..5d3e4705 100644 --- a/src/stcal/jump/twopoint_difference.py +++ b/src/stcal/jump/twopoint_difference.py @@ -168,7 +168,6 @@ def find_crs( (not only_use_ints and nints * ngrps < minimum_sigclip_groups and total_groups < minimum_groups)): log.info("Jump Step was skipped because exposure has less than the minimum number of usable groups") - log.info("Data shape {}".format(str(dat.shape))) dummy = np.zeros((dataa.shape[1] - 1, dataa.shape[2], dataa.shape[3]), dtype=np.float32) return gdq, row_below_gdq, row_above_gdq, 0, dummy @@ -197,7 +196,7 @@ def find_crs( e_jump_4d = first_diffs - median_diffs[np.newaxis, :, :] ratio_all = np.abs(first_diffs - median_diffs[np.newaxis, np.newaxis, :, :]) / \ sigma[np.newaxis, np.newaxis, :, :] - # Test to see if there are enough group to use sigma clipping + # Test to see if there are enough groups to use sigma clipping if (only_use_ints and nints >= minimum_sigclip_groups) or \ (not only_use_ints and total_groups >= minimum_sigclip_groups): log.info(" Jump Step using sigma clip {} greater than {}, rejection threshold {}".format( @@ -247,9 +246,9 @@ def find_crs( # reset sigma so pixels with 0 read noise are not flagged as jumps sigma[np.where(sigma == 0.)] = np.nan - # compute 'ratio' for each group. this is the value that will be - # compared to 'threshold' to classify jumps. subtract the median of - # first_diffs from first_diffs, take the abs. value and divide by sigma. + # compute 'ratio' for each group. this is the value that will be + # compared to 'threshold' to classify jumps. subtract the median of + # first_diffs from first_diffs, take the abs. value and divide by sigma. e_jump = first_diffs - median_diffs[np.newaxis, np.newaxis, :, :] ratio = np.abs(e_jump) / sigma[np.newaxis, np.newaxis, :, :] @@ -293,7 +292,6 @@ def find_crs( int2cr, row2cr, col2cr = np.where( np.logical_and(ndiffs - num_unusable_groups == 2, max_ratio > two_diff_rej_thresh) ) - # get the rows, col pairs for all pixels with at least one CR # all_crs_int = np.concatenate((int4cr, int3cr, int2cr)) all_crs_row = np.concatenate((row4cr, row3cr, row2cr)) diff --git a/tests/test_jump.py b/tests/test_jump.py index 556d554d..c6378713 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -66,7 +66,9 @@ def test_nirspec_saturated_pix(): gdq[0, :, 1, 0] = [0, 0, 0, 0, 0, 0, 2] # run jump detection - gdq, pdq, total_primary_crs, number_extended_events, stddev = detect_jumps(nframes, data, gdq, pdq, err, gain, read_noise, rejection_thresh=4.0, three_grp_thresh=5, + gdq, pdq, total_primary_crs, number_extended_events, stddev = detect_jumps(nframes, data, gdq, pdq, err, + gain, read_noise, rejection_thresh=4.0, + three_grp_thresh=5, four_grp_thresh=6, max_cores='none', max_jump_to_flag_neighbors=200, min_jump_to_flag_neighbors=10, flag_4_neighbors=True, dqflags=DQFLAGS)