From ed89b8319db07126ec67df5750e164584309d71c Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Mon, 1 Apr 2024 15:58:26 -0400 Subject: [PATCH 01/63] First pass at adding the maximum likelihood algorithm to ramp fitting. --- src/stcal/ramp_fitting/ramp_fit.py | 12 ++- tests/test_ramp_fitting_likly_fit.py | 147 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/test_ramp_fitting_likly_fit.py diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index 799c8fb9..6fb4fd2b 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -19,8 +19,9 @@ from astropy import units as u from . import ( - gls_fit, # used only if algorithm is "GLS" - ols_fit, # used only if algorithm is "OLS" + gls_fit, # used only if algorithm is "GLS" + likely_fit, # used only if algorithm is "LIKLEY" + ols_fit, # used only if algorithm is "OLS" ramp_fit_class, ) @@ -141,6 +142,7 @@ def ramp_fit( algorithm : str 'OLS' specifies that ordinary least squares should be used; 'GLS' specifies that generalized least squares should be used. + 'LIKELY' specifies that maximum likelihood should be used. weighting : str 'optimal' specifies that optimal weighting should be used; @@ -222,6 +224,7 @@ def ramp_fit_data( algorithm : str 'OLS' specifies that ordinary least squares should be used; 'GLS' specifies that generalized least squares should be used. + 'LIKELY' specifies that maximum likelihood should be used. weighting : str 'optimal' specifies that optimal weighting should be used; @@ -258,6 +261,11 @@ def ramp_fit_data( ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, max_cores ) opt_info = None + elif algorithm.upper() == "LIKELY": + image_info, integ_info, opt_info = likely_fit.likely_ramp_fit( + ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores + ) + gls_opt_info = None else: # Default to OLS. # Get readnoise array for calculation of variance of noiseless ramps, and diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py new file mode 100644 index 00000000..509d3923 --- /dev/null +++ b/tests/test_ramp_fitting_likly_fit.py @@ -0,0 +1,147 @@ +import numpy as np +import pytest + +from stcal.ramp_fitting.ramp_fit import ramp_fit_class, ramp_fit_data +from stcal.ramp_fitting.ramp_fit_class import RampData + +test_dq_flags = { + "GOOD": 0, + "DO_NOT_USE": 1, + "SATURATED": 2, + "JUMP_DET": 4, + "NO_GAIN_VALUE": 8, + "UNRELIABLE_SLOPE": 16, +} + +GOOD = test_dq_flags["GOOD"] +DO_NOT_USE = test_dq_flags["DO_NOT_USE"] +JUMP_DET = test_dq_flags["JUMP_DET"] +SATURATED = test_dq_flags["SATURATED"] +NO_GAIN_VALUE = test_dq_flags["NO_GAIN_VALUE"] +UNRELIABLE_SLOPE = test_dq_flags["UNRELIABLE_SLOPE"] + +DELIM = "-" * 70 + + +def setup_inputs(dims, gain, rnoise, group_time, frame_time): + """ + Creates test data for testing. All ramp data is zero. + + Parameters + ---------- + dims: tuple + Four dimensions (nints, ngroups, nrows, ncols) + + gain: float + Gain noise + + rnoise: float + Read noise + + group_time: float + Group time + + frame_time: float + Frame time + + Return + ------ + ramp_class: RampClass + A RampClass with all zero data. + + gain: ndarray + A 2-D array for gain noise for each pixel. + + rnoise: ndarray + A 2-D array for read noise for each pixel. + """ + nints, ngroups, nrows, ncols = dims + + ramp_class = ramp_fit_class.RampData() # Create class + + # Create zero arrays according to dimensions + data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + err = np.ones(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + groupdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint8) + pixeldq = np.zeros(shape=(nrows, ncols), dtype=np.uint32) + dark_current = np.zeros(shape=(nrows, ncols), dtype=np.float32) + + + # Set clas arrays + ramp_class.set_arrays(data, err, groupdq, pixeldq, average_dark_current=dark_current) + + # Set class meta + ramp_class.set_meta( + name="MIRI", + frame_time=frame_time, + group_time=group_time, + groupgap=0, + nframes=1, + drop_frames1=0, + ) + + # Set class data quality flags + ramp_class.set_dqflags(test_dq_flags) + + # Set noise arrays + gain = np.ones(shape=(nrows, ncols), dtype=np.float64) * gain + rnoise = np.full((nrows, ncols), rnoise, dtype=np.float32) + + return ramp_class, gain, rnoise + + +def create_blank_ramp_data(dims, var, tm): + """ + Create empty RampData classes, as well as gain and read noise arrays, + based on dimensional, variance, and timing input. + """ + nints, ngroups, nrows, ncols = dims + rnval, gval = var + frame_time, nframes, groupgap = tm + group_time = (nframes + groupgap) * frame_time + + data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + err = np.ones(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) + pixdq = np.zeros(shape=(nrows, ncols), dtype=np.uint32) + gdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint8) + dark_current = np.zeros(shape=(nrows, ncols), dtype = np.float32) + + ramp_data = RampData() + ramp_data.set_arrays(data=data, err=err, groupdq=gdq, pixeldq=pixdq, average_dark_current=dark_current) + ramp_data.set_meta( + name="NIRSpec", + frame_time=frame_time, + group_time=group_time, + groupgap=groupgap, + nframes=nframes, + drop_frames1=None, + ) + ramp_data.set_dqflags(test_dq_flags) + + gain = np.ones(shape=(nrows, ncols), dtype=np.float64) * gval + rnoise = np.ones(shape=(nrows, ncols), dtype=np.float64) * rnval + + return ramp_data, gain, rnoise + + +# ----------------------------------------------------------------------------- + + +def test_basic_ramp(): + nints, ngroups, nrows, ncols = 1, 10, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) From 23684aa42d2baeb476e1fd63e6c9ba2522ce3701 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Tue, 2 Apr 2024 07:05:05 -0400 Subject: [PATCH 02/63] Cleaning up test. --- tests/test_ramp_fitting_likly_fit.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 509d3923..1a2129f8 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -4,6 +4,16 @@ from stcal.ramp_fitting.ramp_fit import ramp_fit_class, ramp_fit_data from stcal.ramp_fitting.ramp_fit_class import RampData +################## DEBUG ################## +# HELP!! +import ipdb +import sys + +sys.path.insert(1, "/Users/kmacdonald/code/common") +from general_funcs import DELIM, dbg_print, array_string + +################## DEBUG ################## + test_dq_flags = { "GOOD": 0, "DO_NOT_USE": 1, @@ -145,3 +155,11 @@ def test_basic_ramp(): slopes, cube, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) + + data = cube[0][0, 0, 0] + ddiff = (ramp_data.data[0, ngroups-1, 0, 0] - ramp_data.data[0, 0, 0, 0]) + check = ddiff / float(ngroups-1) + check = check / ramp_data.group_time + tol = 1.e-5 + diff = abs(data - check) + assert diff < tol From 2d737d54bcb08d74050b691cb5a31f8b413cb938 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 3 Apr 2024 15:36:08 -0400 Subject: [PATCH 03/63] Updating likelihood code. --- src/stcal/ramp_fitting/likely_algo_classes.py | 310 ++++++++ src/stcal/ramp_fitting/likely_fit.py | 738 ++++++++++++++++++ 2 files changed, 1048 insertions(+) create mode 100644 src/stcal/ramp_fitting/likely_algo_classes.py create mode 100644 src/stcal/ramp_fitting/likely_fit.py diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py new file mode 100644 index 00000000..b2828e23 --- /dev/null +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -0,0 +1,310 @@ +import numpy as np +from scipy import special + + +class IntegInfo: + """ + Storage for the integration information for ramp fitting computations. + """ + def __init__(self, nints, nrows, ncols): + """ + Initialize output arrays. + """ + dims = (nints, nrows, ncols) + self.data = np.zeros(shape=dims, dtype=np.float32) + + self.idq = np.zeros(shape=dims, dtype=np.uint32) + + self.var_poisson = np.zeros(shape=dims, dtype=np.float32) + self.var_rnoise = np.zeros(shape=dims, dtype=np.float32) + + self.err = np.zeros(shape=dims, dtype=np.float32) + + def prepare_info(self): + """ + Arrange output arrays as a tuple, which the ramp fit step expects. + """ + return (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) + + def get_results(self, result, integ, row): + """ + Capture the ramp fitting computation. + """ + self.data[integ, row, :] = result.countrate + self.err[integ, row, :] = result.chisq + + +class ImageInfo: + """ + Storage for the observation information for ramp fitting computations. + """ + def __init__(self, nrows, ncols): + dims = (nrows, ncols) + self.data = np.zeros(shape=dims, dtype=np.float32) + + self.idq = np.zeros(shape=dims, dtype=np.uint32) + + self.var_poisson = np.zeros(shape=dims, dtype=np.float32) + self.var_rnoise = np.zeros(shape=dims, dtype=np.float32) + + self.err = np.zeros(shape=dims, dtype=np.float32) + + def prepare_info(self): + return (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) + + +class Ramp_Result: + """ + Contains the ramp fitting results. + """ + def __init__(self): + + self.countrate = None + self.chisq = None + self.uncert = None + self.weights = None + self.pedestal = None + self.uncert_pedestal = None + self.covar_countrate_pedestal = None + + self.countrate_twoomit = None + self.chisq_twoomit = None + self.uncert_twoomit = None + + self.countrate_oneomit = None + self.jumpval_oneomit = None + self.jumpsig_oneomit = None + self.chisq_oneomit = None + self.uncert_oneomit = None + + def __repr__(self): + ostring = f"countrate = \n{self.countrate}" + ostring += f"\nchisq = \n{self.chisq}" + ostring += f"\nucert = \n{self.uncert}" + ''' + ostring += f"\nweights = \n{self.weights}" + ostring += f"\npedestal = \n{self.pedestal}" + ostring += f"\nuncert_pedestal = \n{self.uncert_pedestal}" + ostring += f"\ncovar_countrate_pedestal = \n{self.covar_countrate_pedestal}\n" + + ostring += f"\ncountrate_twoomit = \n{self.countrate_twoomit}" + ostring += f"\nchisq_twoomit = \n{self.chisq_twoomit}" + ostring += f"\nuncert_twoomit = \n{self.uncert_twoomit}" + + ostring += f"\ncountrate_oneomit = \n{self.countrate_oneomit}" + ostring += f"\njumpval_oneomit = \n{self.jumpval_oneomit}" + ostring += f"\njumpsig_oneomit = \n{self.jumpsig_oneomit}" + ostring += f"\nchisq_oneomit = \n{self.chisq_oneomit}" + ostring += f"\nuncert_oneomit = \n{self.uncert_oneomit}" + ''' + + return ostring + + def fill_masked_reads(self, diffs2use): + """ + Replace countrates, uncertainties, and chi squared values that + are NaN because resultant differences were doubly omitted. + For these cases, revert to the corresponding values in with + fewer omitted resultant differences to get the correct values + without double-coundint omissions. + + Arguments: + 1. diffs2use [a 2D array matching self.countrate_oneomit in + shape with zero for resultant differences that + were masked and one for differences that were + not masked] + + This function replaces the relevant entries of + self.countrate_twoomit, self.chisq_twoomit, + self.uncert_twoomit, self.countrate_oneomit, and + self.chisq_oneomit in place. It does not return a value. + + """ + + # replace entries that would be nan (from trying to + # doubly exclude read differences) with the global fits. + + omit = diffs2use == 0 + ones = np.ones(diffs2use.shape) + + self.countrate_oneomit[omit] = (self.countrate * ones)[omit] + self.chisq_oneomit[omit] = (self.chisq * ones)[omit] + self.uncert_oneomit[omit] = (self.uncert * ones)[omit] + + omit = diffs2use[1:] == 0 + + self.countrate_twoomit[omit] = (self.countrate_oneomit[:-1])[omit] + self.chisq_twoomit[omit] = (self.chisq_oneomit[:-1])[omit] + self.uncert_twoomit[omit] = (self.uncert_oneomit[:-1])[omit] + + omit = diffs2use[:-1] == 0 + + self.countrate_twoomit[omit] = (self.countrate_oneomit[1:])[omit] + self.chisq_twoomit[omit] = (self.chisq_oneomit[1:])[omit] + self.uncert_twoomit[omit] = (self.uncert_oneomit[1:])[omit] + + +class Covar: + """ + class Covar holding read and photon noise components of alpha and + beta and the time intervals between the resultant midpoints + """ + def __init__(self, readtimes, pedestal=False): + """ + Compute alpha and beta, the diagonal and off-diagonal elements of + the covariance matrix of the resultant differences, and the time + intervals between the resultant midpoints. + + Arguments: + 1. readtimes [list of values or lists for the times of reads. If + a list of lists, times for reads that are averaged + together to produce a resultant.] + Optional arguments: + 2. pedestal [boolean: does the covariance matrix include the terms + for the first resultant? This is needed if fitting + for the pedestal (i.e. the reset value). Default + False. ] + """ + # Equations (4) and (11) in paper 1. + mean_t, tau, N, delta_t = self._compute_means_and_taus(readtimes, pedestal) + + self.pedestal = pedestal + self.delta_t = delta_t + self.mean_t = mean_t + self.tau = tau + self.Nreads = N + + # Equations (28) and (29) in paper 1. + self._compute_alphas_and_betas(mean_t, tau, N, delta_t) + + if pedestal: + # Equations (32) and (33) in paper 1. + self._compute_pedestal(mean_t, tau, N, delta_t) + + def _compute_means_and_taus(self, readtimes, pedestal): + mean_t = [] # mean time of the resultant as defined in the paper + tau = [] # variance-weighted mean time of the resultant + N = [] # Number of reads per resultant + + for times in readtimes: + mean_t += [np.mean(times)] + + if hasattr(times, "__len__"): + # eqn 11 + N += [len(times)] + k = np.arange(1, N[-1] + 1) + if False: + tau += [ + 1 + / N[-1] ** 2 + * np.sum((2 * N[-1] + 1 - 2 * k) * np.array(times)) + ] + # tau += [(np.sum((2*N[-1] + 1 - 2*k)*np.array(times))) / N[-1]**2] + else: + length = N[-1] + tmp0 = (2 * length + 1) - (2 * k) + sm = np.sum(tmp0 * np.array(times)) + tmp = sm / length**2 + tau.append(tmp) + else: + tau += [times] + N += [1] + + # readtimes is a list of lists, so mean_t is the list of each mean of each list. + mean_t = np.array(mean_t) + tau = np.array(tau) + N = np.array(N) + delta_t = mean_t[1:] - mean_t[:-1] + + return mean_t, tau, N, delta_t + + def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): + self.alpha_readnoise = 1 / N[:-1] + 1 / (N[1:]) * delta_t**2 + self.beta_readnoise = -1 / (N[1:-1] * delta_t[1:] * delta_t[:-1]) + + self.alpha_phnoise = (tau[:-1] + tau[1:] - 2 * mean_t[:-1]) / delta_t**2 + self.beta_phnoise = (mean_t[1:-1] - tau[1:-1]) / (delta_t[1:] * delta_t[:-1]) + + def _compute_pedestal(self, mean_t, tau, N, delta_t): + # If we want the reset value we need to include the first + # resultant. These are the components of the variance and + # covariance for the first resultant. + arn = list(self.alpha_readnoise) + brn = list(self.beta_readnoise) + ahn = list(self.alpha_phnoise) + bhn = list(self.beta_phnoise) + + self.alpha_readnoise = np.array([1 / (N[0] * mean_t[0] ** 2)] + arn) + self.beta_readnoise = np.array([-1 / (N[0] * mean_t[0] * delta_t[0])] + brn) + self.alpha_phnoise = np.array([tau[0] / mean_t[0] ** 2] + ahn) + self.beta_phnoise = np.array( + [(mean_t[0] - tau[0]) / (mean_t[0] * delta_t[0])] + bhn + ) + + def calc_bias(self, countrates, sig, cvec, da=1e-7): + """ + Calculate the bias in the best-fit count rate from estimating the + covariance matrix. This calculation is derived in the paper. + + Section 5 of paper 1. + + Arguments: + 1. countrates [array of count rates at which the bias is desired] + 2. sig [float, single read noise] + 3. cvec [weight vector on resultant differences for initial + estimation of count rate for the covariance matrix. + Will be renormalized inside this function.] + Optional argument: + 4. da [float, fraction of the count rate plus sig**2 to use for finite + difference estimate of the derivative. Default 1e-7.] + + Returns: + 1. bias [array, bias of the best-fit count rate from using cvec + plus the observed resultants to estimate the covariance + matrix] + + """ + if self.pedestal: + raise ValueError( + "Cannot compute bias with a Covar class that includes a pedestal fit." + ) + + alpha = countrates[np.newaxis, :] * self.alpha_phnoise[:, np.newaxis] + alpha += sig**2 * self.alpha_readnoise[:, np.newaxis] + beta = countrates[np.newaxis, :] * self.beta_phnoise[:, np.newaxis] + beta += sig**2 * self.beta_readnoise[:, np.newaxis] + + # we only want the weights; it doesn't matter what the count rates are. + n = alpha.shape[0] + z = np.zeros((len(cvec), len(countrates))) + result_low_a = fit_ramps(z, self, sig, countrateguess=countrates) + + # try to avoid problems with roundoff error + da_incr = da * (countrates[np.newaxis, :] + sig**2) + + dalpha = da_incr * self.alpha_phnoise[:, np.newaxis] + dbeta = da_incr * self.beta_phnoise[:, np.newaxis] + result_high_a = fit_ramps(z, self, sig, countrateguess=countrates + da_incr) + # finite difference approximation to dw/da + + dw_da = (result_high_a.weights - result_low_a.weights) / da_incr + + bias = np.zeros(len(countrates)) + c = cvec / np.sum(cvec) + + for i in range(len(countrates)): + + C = np.zeros((n, n)) + for j in range(n): + C[j, j] = alpha[j, i] + for j in range(n - 1): + C[j + 1, j] = C[j, j + 1] = beta[j, i] + + bias[i] = np.linalg.multi_dot([c[np.newaxis, :], C, dw_da[:, i]]) + + sig_a = np.sqrt( + np.linalg.multi_dot([c[np.newaxis, :], C, c[:, np.newaxis]]) + ) + bias[i] *= 0.5 * (1 + special.erf(countrates[i] / sig_a / 2**0.5)) + + return bias diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py new file mode 100644 index 00000000..971e83ac --- /dev/null +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -0,0 +1,738 @@ +#! /usr/bin/env python + +import logging +import multiprocessing +import time +import warnings +from multiprocessing import cpu_count + +import numpy as np + +from . import ramp_fit_class, utils +from .likely_algo_classes import IntegInfo, ImageInfo, Ramp_Result, Covar + +################## DEBUG ################## +# HELP!! +import ipdb +import sys + +sys.path.insert(1, "/Users/kmacdonald/code/common") +from general_funcs import DELIM, dbg_print, array_string + +################## DEBUG ################## + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + +BUFSIZE = 1024 * 300000 # 300Mb cache size for data section + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +def likely_ramp_fit(ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores): + """ + Setup the inputs to ols_ramp_fit with and without multiprocessing. The + inputs will be sliced into the number of cores that are being used for + multiprocessing. Because the data models cannot be pickled, only numpy + arrays are passed and returned as parameters to ols_ramp_fit. + + Parameters + ---------- + ramp_data : RampData + Input data necessary for computing ramp fitting. + + buffsize : int + size of data section (buffer) in bytes (not used) + + save_opt : bool + calculate optional fitting results + + readnoise_2d : ndarray + readnoise for all pixels + + gain_2d : ndarray + gain for all pixels + + algorithm : str + 'OLS' specifies that ordinary least squares should be used; + 'GLS' specifies that generalized least squares should be used. + + weighting : str + 'optimal' specifies that optimal weighting should be used; + currently the only weighting supported. + + max_cores : str + Number of cores to use for multiprocessing. If set to 'none' (the default), + then no multiprocessing will be done. The other allowable values are 'quarter', + 'half', and 'all'. This is the fraction of cores to use for multi-proc. The + total number of cores includes the SMT cores (Hyper Threading for Intel). + + Returns + ------- + image_info : tuple + The tuple of computed ramp fitting arrays. + + integ_info : tuple + The tuple of computed integration fitting arrays. + + opt_info : tuple + The tuple of computed optional results arrays for fitting. + """ + image_info, integ_info, opt_info = None, None, None + + nints, ngroups, nrows, ncols = ramp_data.data.shape + + # XXX Maybe make this more general. Since groups are evenly space + # in JWST, the readtimes are simply uniformly spaced times based + # on group time. With the knowledge of frame time and the number + # of frames, this may be able to be more general. Ask someone + # about this. + readtimes = [(k+1) * ramp_data.group_time for k in range(ngroups)] + covar = Covar(readtimes) + integ_class = IntegInfo(nints, nrows, ncols) + + for integ in range(nints): + data = ramp_data.data[integ, :, :, :] + diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] + + for row in range(nrows): + result = fit_ramps(diff[:, row], covar, readnoise_2d[row]) + integ_class.get_results(result, integ, row) + + integ_info = integ_class.prepare_info() + + return image_info, integ_info, opt_info + + +def inital_countrateguess(Cov, diffs, diffs2use): + """ + Compute the initial count rate. + + Parameters + ---------- + Cov : Covar + The class that computes and contains the covariance matrix info. + + diffs : ndarray + The group differences of the data (ngroups-1, nrows, ncols). + + diffs2use : ndarray + Boolean mask determining with group differences to use (ngroups-1, ncols). + """ + # initial guess for count rate is the average of the unmasked + # group differences unless otherwise specified. + if Cov.pedestal: + num = np.sum((diffs * diffs2use)[1:], axis=0) + den = np.sum(diffs2use[1:], axis=0) + countrateguess = num / den + ''' + countrateguess = np.sum((diffs * diffs2use)[1:], axis=0) / np.sum( + diffs2use[1:], axis=0 + ) + ''' + else: + num = np.sum((diffs * diffs2use), axis=0) + den = np.sum(diffs2use, axis=0) + countrateguess = num / den + ''' + countrateguess = np.sum((diffs * diffs2use), axis=0) / np.sum( + diffs2use, axis=0 + ) + ''' + countrateguess *= countrateguess > 0 + + return countrateguess + + +# RAMP FITTING BEGIN +def fit_ramps( + diffs, + covar, + rnoise, + countrateguess=None, + diffs2use=None, + detect_jumps=False, + resetval=0, + resetsig=np.inf, + rescale=True, +): + """ + Function fit_ramps on a row of pixels. Fits ramps to read differences + using the covariance matrix for the read differences as given by the + diagonal elements and the off-diagonal elements. + + Parameters + ---------- + diffs : ndarray + The group differences of the data (ngroups-1, nrows, ncols). + + covar : Covar + The class that computes and contains the covariance matrix info. + + rnoise : ndarray + The read noise (ncols,). XXX - the name should be changed. + + countrateguess : ndarray + Count rate estimates used to estimate the covariance matrix. + Optional, default is None. + + diffs2use : ndarray + Boolean mask determining with group differences to use (ngroups-1, ncols). + Optional, default is None, which results in a mask of all 1's. + + detect_jumps : boolean + Run jump detection. + Optional, default is False. + + resetval : float or ndarray + Priors on the reset values. Irrelevant unless pedestal is True. If an + ndarray, it has dimensions (ncols). + Opfional, default is 0. + + resetsig : float or ndarray + Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. + Optional, default np.inf, i.e., reset values have flat priors. + + rescale : boolean + Scale the covariance matrix internally to avoid possible + overflow/underflow problems for long ramps. + Optional, default is True. + + Returns + ------- + result : Ramp_Result + Holds computed ramp fitting information. XXX - rename + """ + # XXX + # this needs to be refactored into a well written function, + # instead of meandering crap, hundreds of lines long. The + # next function is at line 544. + + if diffs2use is None: + # Use all diffs + diffs2use = np.ones(diffs.shape, np.uint8) + + # diffs is (ngroups, ncols) of the current row + if countrateguess is None: + countrateguess = inital_countrateguess(covar, diffs, diffs2use) + + alpha, beta, scale = compute_abs(countrateguess, rnoise, covar, rescale) + ndiffs, npix = alpha.shape + + # Mask group differences that should be ignored. This is half + # of what we need to do to mask these group differences; the + # rest comes later. + diff_mask = diffs * diffs2use + beta = beta * diffs2use[1:] * diffs2use[:-1] + + # All definitions and formulas here are in the paper. + # --- Till line 284: Paper 1 section 4 + theta = compute_thetas(ndiffs, npix, alpha, beta) # EQNs 38-40 + phi = compute_phis(ndiffs, npix, alpha, beta) # EQNs 41-43 + + sgn = np.ones((ndiffs, npix)) + sgn[::2] = -1 + + Phi = compute_Phis(ndiffs, npix, beta, phi, sgn) # EQN 46 + PhiD = compute_PhiDs(ndiffs, npix, beta, phi, sgn, diff_mask) # EQN ?? + Theta = compute_Thetas(ndiffs, npix, beta, theta, sgn) # EQN 47 + ThetaD = compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask) # EQN 48 + + dB, dC, A, B, C = matrix_computations( + ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD) + + result = get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig) + # --- Beginning at line 250: Paper 1 section 4 + + # The code below computes the best chi squared, best-fit slope, + # and its uncertainty leaving out each group difference in + # turn. There are ndiffs possible differences that can be + # omitted. + # + # Then do it omitting two consecutive reads. There are ndiffs-1 + # possible pairs of adjacent reads that can be omitted. + # + # This approach would need to be modified if also fitting the + # pedestal, so that condition currently triggers an error. The + # modifications would make the equations significantly more + # complicated; the matrix equations to be solved by hand would be + # larger. + + # XXX - This needs to get moved into a separate function. This section should + # be separated anyway, since it's a completely separate function, but the + # code itself should be further broken down, as it's a meandering mess + # also. Far too complicated for a single function. + # Paper II, sections 3.1 and 3.2 + if detect_jumps: + + # The algorithms below do not work if we are computing the + # pedestal here. + + if covar.pedestal: + raise ValueError( + "Cannot use jump detection algorithm when fitting pedestals." + ) + + # Diagonal elements of the inverse covariance matrix + + Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] + Cinv_diag *= diffs2use + + # Off-diagonal elements of the inverse covariance matrix + # one spot above and below for the case of two adjacent + # differences to be masked + + Cinv_offdiag = -beta * theta[:-2] * phi[2:] / theta[ndiffs] + + # Equations in the paper: best-fit a, b + # + # Catch warnings in case there are masked group + # differences, since these will be overwritten later. No need + # to warn about division by zero here. + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + a = (Cinv_diag * B - dB * dC) / (C * Cinv_diag - dC**2) + b = (dB - a * dC) / Cinv_diag + + result.countrate_oneomit = a + result.jumpval_oneomit = b + + # Use the best-fit a, b to get chi squared + + result.chisq_oneomit = ( + A + + a**2 * C + - 2 * a * B + + b**2 * Cinv_diag + - 2 * b * dB + + 2 * a * b * dC + ) + # invert the covariance matrix of a, b to get the uncertainty on a + result.uncert_oneomit = np.sqrt(Cinv_diag / (C * Cinv_diag - dC**2)) + result.jumpsig_oneomit = np.sqrt(C / (C * Cinv_diag - dC**2)) + + result.chisq_oneomit /= scale + result.uncert_oneomit *= np.sqrt(scale) + result.jumpsig_oneomit *= np.sqrt(scale) + + # Now for two omissions in a row. This is more work. Again, + # all equations are in the paper. I first define three + # factors that will be used more than once to save a bit of + # computational effort. + + cpj_fac = dC[:-1] ** 2 - C * Cinv_diag[:-1] + cjck_fac = dC[:-1] * dC[1:] - C * Cinv_offdiag + bcpj_fac = B * dC[:-1] - dB[:-1] * C + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # best-fit a, b, c + c = bcpj_fac / cpj_fac - (B * dC[1:] - dB[1:] * C) / cjck_fac + c /= cjck_fac / cpj_fac - (dC[1:] ** 2 - C * Cinv_diag[1:]) / cjck_fac + b = (bcpj_fac - c * cjck_fac) / cpj_fac + a = (B - b * dC[:-1] - c * dC[1:]) / C + result.countrate_twoomit = a + + # best-fit chi squared + result.chisq_twoomit = ( + A + a**2 * C + b**2 * Cinv_diag[:-1] + c**2 * Cinv_diag[1:] + ) + result.chisq_twoomit -= 2 * a * B + 2 * b * dB[:-1] + 2 * c * dB[1:] + result.chisq_twoomit += ( + 2 * a * b * dC[:-1] + 2 * a * c * dC[1:] + 2 * b * c * Cinv_offdiag + ) + result.chisq_twoomit /= scale + + # uncertainty on the slope from inverting the (a, b, c) + # covariance matrix + fac = Cinv_diag[1:] * Cinv_diag[:-1] - Cinv_offdiag**2 + term2 = dC[:-1] * (dC[:-1] * Cinv_diag[1:] - Cinv_offdiag * dC[1:]) + term3 = dC[1:] * (dC[:-1] * Cinv_offdiag - Cinv_diag[:-1] * dC[1:]) + result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) + result.uncert_twoomit *= np.sqrt(scale) + + result.fill_masked_reads(diffs2use) + + return result +# RAMP FITTING END + +def compute_abs(countrateguess, rnoise, covar, rescale): + """ + Compute alpha, beta, and scale needed for ramp fit. + Elements of the covariance matrix. + Are these EQNs 32 and 33? + + Parameters + ---------- + countrateguess : ndarray + Initial guess (ncols,) + + rnoise : ndarray + Readnoise (ncols,) + + covar : Covar + The class that computes and contains the covariance matrix info. + + rescale : bool + Determination to rescale covariance matrix. + + Returns + ------- + alpha : ndarray + Diagonal of covariance matrix. + + beta : ndarray + Off diagonal of covariance matrix. + + scale : ndarray or integer + Overflow/underflow prevention scale. + + """ + alpha = countrateguess * covar.alpha_phnoise[:, np.newaxis] + alpha += rnoise**2 * covar.alpha_readnoise[:, np.newaxis] + beta = countrateguess * covar.beta_phnoise[:, np.newaxis] + beta += rnoise**2 * covar.beta_readnoise[:, np.newaxis] + + # rescale the covariance matrix to a determinant of order 1 to + # avoid possible overflow/underflow. The uncertainty and chi + # squared value will need to be scaled back later. + if rescale: + scale = np.exp(np.mean(np.log(alpha), axis=0)) + else: + scale = 1 + + alpha /= scale + beta /= scale + + return alpha, beta, scale + +def compute_thetas(ndiffs, npix, alpha, beta): + """ + EQNs 38-40 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + alpha : ndarray + Diagonal of covariance matrix. + + beta : ndarray + Off diagonal of covariance matrix. + + Returns + ------- + theta : ndarray + """ + theta = np.ones((ndiffs + 1, npix)) + theta[1] = alpha[0] + for i in range(2, ndiffs + 1): + theta[i] = alpha[i - 1] * theta[i - 1] - beta[i - 2] ** 2 * theta[i - 2] + return theta + + +def compute_phis(ndiffs, npix, alpha, beta): + """ + EQNs 41-43 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + alpha : ndarray + Diagonal of covariance matrix. + + beta : ndarray + Off diagonal of covariance matrix. + + Returns + ------- + phi : ndarray + """ + phi = np.ones((ndiffs + 1, npix)) + phi[ndiffs - 1] = alpha[ndiffs - 1] + for i in range(ndiffs - 2, -1, -1): + phi[i] = alpha[i] * phi[i + 1] - beta[i] ** 2 * phi[i + 2] + return phi + + +def compute_Phis(ndiffs, npix, beta, phi, sgn): + """ + EQN 46 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + alpha : ndarray + Diagonal of covariance matrix. + + beta : ndarray + Off diagonal of covariance matrix. + + sgn : ndarray + Oscillating 1, -1 sequence. + + Returns + ------- + Phi : ndarray + """ + Phi = np.zeros((ndiffs, npix)) + for i in range(ndiffs - 2, -1, -1): + Phi[i] = Phi[i + 1] * beta[i] + sgn[i + 1] * beta[i] * phi[i + 2] + return Phi + +def compute_PhiDs(ndiffs, npix, beta, phi, sgn, diff_mask): + """ + EQN 4, Paper II + This one is defined later in the paper and is used for jump + detection and pedestal fitting. + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + beta : ndarray + Off diagonal of covariance matrix. + + phi : ndarray + Intermediate computation. + + sgn : ndarray + Oscillating 1, -1 sequence. + + diff_mask : ndarray + Mask of differences used. + + Returns + ------- + PhiD: ndarray + """ + PhiD = np.zeros((ndiffs, npix)) + for i in range(ndiffs - 2, -1, -1): + PhiD[i] = (PhiD[i + 1] + sgn[i + 1] * diff_mask[i + 1] * phi[i + 2]) * beta[i] + return PhiD + + +def compute_Thetas(ndiffs, npix, beta, theta, sgn): + """ + EQN 47 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + beta : ndarray + Off diagonal of covariance matrix. + + theta : ndarray + Intermediate computation. + + sgn : ndarray + Oscillating 1, -1 sequence. + + Returns + ------- + Theta : ndarray + """ + Theta = np.zeros((ndiffs, npix)) + Theta[0] = -theta[0] + for i in range(1, ndiffs): + Theta[i] = Theta[i - 1] * beta[i - 1] + sgn[i] * theta[i] + return Theta + + +def compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask): + """ + EQN 48 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + beta : ndarray + Off diagonal of covariance matrix. + + theta : ndarray + Intermediate computation. + + sgn : ndarray + Oscillating 1, -1 sequence. + + diff_mask : ndarray + Mask of differences used. + + Returns + ------- + ThetaD : ndarray + """ + ThetaD = np.zeros((ndiffs + 1, npix)) + ThetaD[1] = -diff_mask[0] * theta[0] + for i in range(1, ndiffs): + ThetaD[i + 1] = beta[i - 1] * ThetaD[i] + sgn[i] * diff_mask[i] * theta[i] + return ThetaD + + +def matrix_computations( + ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD): + """ + Computing matrix computations needed for ramp fitting. + EQNs 61-63, 71, 75 + + Parameters + ---------- + ndiffs : int + Number of differences. + + npix : int + Number of columns in a row. + + sgn : ndarray + Oscillating 1, -1 sequence. + + diff_mask : ndarray + Mask of differences used. + + diff2use : ndarray + Masked differences. + + beta : ndarray + Off diagonal of covariance matrix. + + phi : ndarray + Intermediate computation. + + Phi : ndarray + Intermediate computation. + + PhiD : ndarray + Intermediate computation. + + theta : ndarray + Intermediate computation. + + Theta : ndarray + Intermediate computation. + + ThetaD : ndarray + Intermediate computation. + + Returns + ------- + dB : ndarray + + dC : ndarray + + A : ndarray + + B : ndarray + + C : ndarray + + """ + beta_extended = np.ones((ndiffs, npix)) + beta_extended[1:] = beta + + # C' and B' in the paper + + dC = sgn / theta[ndiffs] * (phi[1:] * Theta + theta[:-1] * Phi) + dC *= diffs2use # EQN 71 + + dB = sgn / theta[ndiffs] * (phi[1:] * ThetaD[1:] + theta[:-1] * PhiD) # EQN 75 + + # {\cal A}, {\cal B}, {\cal C} in the paper + + # EQNs 61-63 + A = 2 * np.sum(diff_mask * sgn / theta[-1] * beta_extended * phi[1:] * ThetaD[:-1], axis=0) + A += np.sum(diff_mask**2 * theta[:-1] * phi[1:] / theta[ndiffs], axis=0) + + B = np.sum(diff_mask * dC, axis=0) + C = np.sum(dC, axis=0) + + return dB, dC, A, B, C + + +def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, Cov, resetval, resetsig): + result = Ramp_Result() + + # Finally, save the best-fit count rate, chi squared, uncertainty + # in the count rate, and the weights used to combine the + # groups. + + if not Cov.pedestal: + result.countrate = B / C + result.chisq = (A - B**2 / C) / scale + result.uncert = np.sqrt(scale / C) + result.weights = dC / C + + # If we are computing the pedestal, then we use the other formulas + # in the paper. + + else: + dt = Cov.mean_t[0] + Cinv_11 = theta[0] * phi[1] / theta[ndiffs] + + # Calculate the pedestal and slope using the equations in the paper. + # Do not compute weights for this case. + + b = dB[0] * C * dt - B * dC[0] * dt + dt**2 * C * resetval / resetsig**2 + b /= C * Cinv_11 - dC[0] ** 2 + dt**2 * C / resetsig**2 + a = B / C - b * dC[0] / C / dt + result.pedestal = b + result.countrate = a + result.chisq = A + a**2 * C + b**2 / dt**2 * Cinv_11 + result.chisq += -2 * b / dt * dB[0] - 2 * a * B + 2 * a * b / dt * dC[0] + result.chisq /= scale + + # elements of the inverse covariance matrix + M = [C, dC[0] / dt, Cinv_11 / dt**2 + 1 / resetsig**2] + detM = M[0] * M[-1] - M[1] ** 2 + result.uncert = np.sqrt(scale * M[-1] / detM) + result.uncert_pedestal = np.sqrt(scale * M[0] / detM) + result.covar_countrate_pedestal = -scale * M[1] / detM + + return result + + +################################################################################ +################################## DEBUG ####################################### + +def dbg_print_info(group_time, readtimes, data, diff): + print(DELIM) + print(f"group_time = {group_time}") + print(DELIM) + print(f"readtimes = {array_string(np.array(readtimes))}") + print(DELIM) + print(f"data = {array_string(data[:, 0, 0])}") + print(DELIM) + data_gt = data / group_time + print(f"data / gt = {array_string(data_gt[:, 0, 0])}") + print(DELIM) + print(f"diff = {array_string(diff[:, 0, 0])}") + print(DELIM) From e548b0839695af17725df072a7370283e0ec0368 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 3 Apr 2024 15:36:41 -0400 Subject: [PATCH 04/63] Updating test. --- tests/test_ramp_fitting_likly_fit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 1a2129f8..69fac89f 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -138,6 +138,9 @@ def create_blank_ramp_data(dims, var, tm): def test_basic_ramp(): + """ + Test a basic ramp with a linear progression up the ramp. + """ nints, ngroups, nrows, ncols = 1, 10, 1, 1 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 4, 1 From f802ea24938b865c56d99a143808890966018b04 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Mon, 8 Apr 2024 11:16:31 -0400 Subject: [PATCH 05/63] Updating comments and computing diffs2use mask for ramp segmentation. --- src/stcal/ramp_fitting/likely_algo_classes.py | 170 ++++++++++---- src/stcal/ramp_fitting/likely_fit.py | 214 ++++++++++++++---- src/stcal/ramp_fitting/ramp_fit.py | 4 + src/stcal/ramp_fitting/ramp_fit_class.py | 1 + tests/test_ramp_fitting_likly_fit.py | 78 ++++++- 5 files changed, 383 insertions(+), 84 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index b2828e23..a3ccf3b8 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -9,6 +9,17 @@ class IntegInfo: def __init__(self, nints, nrows, ncols): """ Initialize output arrays. + + Parameters + ---------- + nints : int + The number of integrations in the data. + + nrows : int + The number of rows in the data. + + ncols : int + The number of columns in the data. """ dims = (nints, nrows, ncols) self.data = np.zeros(shape=dims, dtype=np.float32) @@ -29,16 +40,35 @@ def prepare_info(self): def get_results(self, result, integ, row): """ Capture the ramp fitting computation. + + Parameters + ---------- + result : Ramp_Result + Holds computed ramp fitting information. XXX - rename + + integ : int + The current integration being operated on. + + row : int + The current row being operated on. """ self.data[integ, row, :] = result.countrate self.err[integ, row, :] = result.chisq class ImageInfo: - """ - Storage for the observation information for ramp fitting computations. - """ def __init__(self, nrows, ncols): + """ + Storage for the observation information for ramp fitting computations. + + Parameters + ---------- + nrows : int + The number of rows in the data. + + ncols : int + The number of columns in the data. + """ dims = (nrows, ncols) self.data = np.zeros(shape=dims, dtype=np.float32) @@ -50,15 +80,17 @@ def __init__(self, nrows, ncols): self.err = np.zeros(shape=dims, dtype=np.float32) def prepare_info(self): + """ + Package the data to be returned from ramp fitting. + """ return (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) class Ramp_Result: - """ - Contains the ramp fitting results. - """ def __init__(self): - + """ + Contains the ramp fitting results. + """ self.countrate = None self.chisq = None self.uncert = None @@ -78,6 +110,9 @@ def __init__(self): self.uncert_oneomit = None def __repr__(self): + """ + Return string of information about the class. + """ ostring = f"countrate = \n{self.countrate}" ostring += f"\nchisq = \n{self.chisq}" ostring += f"\nucert = \n{self.uncert}" @@ -108,22 +143,20 @@ def fill_masked_reads(self, diffs2use): fewer omitted resultant differences to get the correct values without double-coundint omissions. - Arguments: - 1. diffs2use [a 2D array matching self.countrate_oneomit in - shape with zero for resultant differences that - were masked and one for differences that were - not masked] - This function replaces the relevant entries of self.countrate_twoomit, self.chisq_twoomit, self.uncert_twoomit, self.countrate_oneomit, and self.chisq_oneomit in place. It does not return a value. + Parameters + ---------- + diffs2use : ndarray + A 2D array matching self.countrate_oneomit in shape with zero + for resultant differences that were masked and one for + differences that were not masked. """ - # replace entries that would be nan (from trying to # doubly exclude read differences) with the global fits. - omit = diffs2use == 0 ones = np.ones(diffs2use.shape) @@ -155,15 +188,17 @@ def __init__(self, readtimes, pedestal=False): the covariance matrix of the resultant differences, and the time intervals between the resultant midpoints. - Arguments: - 1. readtimes [list of values or lists for the times of reads. If - a list of lists, times for reads that are averaged - together to produce a resultant.] - Optional arguments: - 2. pedestal [boolean: does the covariance matrix include the terms - for the first resultant? This is needed if fitting - for the pedestal (i.e. the reset value). Default - False. ] + Parameters + ---------- + readtimes : list + List of values or lists for the times of reads. If a list of + lists, times for reads that are averaged together to produce + a resultant. + + pedestal : boolean + Does the covariance matrix include the terms for the first + resultant? This is needed if fitting for the pedestal (i.e. + the reset value). Optional parameter Default: False. """ # Equations (4) and (11) in paper 1. mean_t, tau, N, delta_t = self._compute_means_and_taus(readtimes, pedestal) @@ -182,6 +217,21 @@ def __init__(self, readtimes, pedestal=False): self._compute_pedestal(mean_t, tau, N, delta_t) def _compute_means_and_taus(self, readtimes, pedestal): + """ + Computes the means and taus of defined in EQNs 4 and 11 in paper 1. + + Parameters + ---------- + readtimes : list + List of values or lists for the times of reads. If a list of + lists, times for reads that are averaged together to produce + a resultant. + + pedestal : boolean + Does the covariance matrix include the terms for the first + resultant? This is needed if fitting for the pedestal (i.e. + the reset value). + """ mean_t = [] # mean time of the resultant as defined in the paper tau = [] # variance-weighted mean time of the resultant N = [] # Number of reads per resultant @@ -219,6 +269,23 @@ def _compute_means_and_taus(self, readtimes, pedestal): return mean_t, tau, N, delta_t def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): + """ + Computes the means and taus of defined in EQNs 28 and 29 in paper 1. + + Parameters + ---------- + mean_t : ndarray + The means of the reads for each group. + + tau : ndarray + Intermediate computation. + + N : ndarray + The number of reads in each group. + + delta_t : ndarray + The group differences of integration ramps. + """ self.alpha_readnoise = 1 / N[:-1] + 1 / (N[1:]) * delta_t**2 self.beta_readnoise = -1 / (N[1:-1] * delta_t[1:] * delta_t[:-1]) @@ -226,6 +293,23 @@ def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): self.beta_phnoise = (mean_t[1:-1] - tau[1:-1]) / (delta_t[1:] * delta_t[:-1]) def _compute_pedestal(self, mean_t, tau, N, delta_t): + """ + Computes the means and taus of defined in EQNs 28 and 29 in paper 1. + + Parameters + ---------- + mean_t : ndarray + The means of the reads for each group. + + tau : ndarray + Intermediate computation. + + N : ndarray + The number of reads in each group. + + delta_t : ndarray + The group differences of integration ramps. + """ # If we want the reset value we need to include the first # resultant. These are the components of the variance and # covariance for the first resultant. @@ -246,23 +330,31 @@ def calc_bias(self, countrates, sig, cvec, da=1e-7): Calculate the bias in the best-fit count rate from estimating the covariance matrix. This calculation is derived in the paper. - Section 5 of paper 1. + Section 5 of paper 1. XXX Not sure when to use this method. Arguments: - 1. countrates [array of count rates at which the bias is desired] - 2. sig [float, single read noise] - 3. cvec [weight vector on resultant differences for initial - estimation of count rate for the covariance matrix. - Will be renormalized inside this function.] - Optional argument: - 4. da [float, fraction of the count rate plus sig**2 to use for finite - difference estimate of the derivative. Default 1e-7.] - - Returns: - 1. bias [array, bias of the best-fit count rate from using cvec - plus the observed resultants to estimate the covariance - matrix] - + Parameters + ---------- + countrates : ndarray + Array of count rates at which the bias is desired. + + sig : float + Single read noise] + + cvec : ndarray + Weight vector on resultant differences for initial estimation + of count rate for the covariance matrix. Will be renormalized + inside this function. + + da : float + Fraction of the count rate plus sig**2 to use for finite difference + estimate of the derivative. Optional parameter. Default 1e-7. + + Returns + ------- + bias : ndarray + Bias of the best-fit count rate from using cvec plus the observed + resultants to estimate the covariance matrix. """ if self.pedestal: raise ValueError( diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 971e83ac..4446348b 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -30,7 +30,9 @@ log.setLevel(logging.DEBUG) -def likely_ramp_fit(ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores): +def likely_ramp_fit( + ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores +): """ Setup the inputs to ols_ramp_fit with and without multiprocessing. The inputs will be sliced into the number of cores that are being used for @@ -83,12 +85,12 @@ def likely_ramp_fit(ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weight nints, ngroups, nrows, ncols = ramp_data.data.shape - # XXX Maybe make this more general. Since groups are evenly space - # in JWST, the readtimes are simply uniformly spaced times based - # on group time. With the knowledge of frame time and the number - # of frames, this may be able to be more general. Ask someone - # about this. - readtimes = [(k+1) * ramp_data.group_time for k in range(ngroups)] + if ramp_data.read_pattern is None: + # XXX Not sure if this is the right way to do things. + readtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] + else: + readtimes = read_data.read_pattern + covar = Covar(readtimes) integ_class = IntegInfo(nints, nrows, ncols) @@ -97,7 +99,8 @@ def likely_ramp_fit(ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weight diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] for row in range(nrows): - result = fit_ramps(diff[:, row], covar, readnoise_2d[row]) + d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) + result = fit_ramps(diff[:, row], covar, readnoise_2d[row], diffs2use=d2use) integ_class.get_results(result, integ, row) integ_info = integ_class.prepare_info() @@ -105,13 +108,73 @@ def likely_ramp_fit(ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weight return image_info, integ_info, opt_info -def inital_countrateguess(Cov, diffs, diffs2use): +def determine_diffs2use(ramp_data, integ, row, diffs): + """ + Compute the diffs2use mask based on DQ flags of a row. + + Parameters + ---------- + ramp_data : RampData + Input data necessary for computing ramp fitting. + + integ : int + The current integration being processed. + + row : int + The current row being processed. + + diffs : ndarray + The group differences of the data array for a given integration and row + (ngroups-1, ncols). + + Returns + ------- + d2use : ndarray + A boolean array definined the segmented ramps for each pixel in a row. + (ngroups-1, ncols) + """ + _, ngroups, _, ncols = ramp_data.data.shape + dq = np.zeros(shape=(ngroups, ncols), dtype=np.uint8) + dq[:, :] = ramp_data.groupdq[integ, :, row, :] + d2use = np.ones(shape=diffs.shape, dtype=np.uint8) + + # The JUMP_DET is handled different than other group DQ flags. + jmp = np.uint8(ramp_data.flags_jump_det) + other_flags = ~jmp + + # Find all non-jump flags + oflags_locs = np.zeros(shape=dq.shape, dtype=np.uint8) + wh_of = np.where(np.bitwise_and(dq, other_flags)) + oflags_locs[wh_of] = 1 + + # Find all jump flags + jmp_locs = np.zeros(shape=dq.shape, dtype=np.uint8) + wh_j = np.where(np.bitwise_and(dq, jmp)) + jmp_locs[wh_j] = 1 + + del wh_of, wh_j + + # Based on flagging, exclude differences associated with flagged groups. + + # If a jump occurs at group k, then the difference + # group[k] - group[k-1] is excluded. + d2use[jmp_locs[1:, :]==1] = 0 + + # If a non-jump flag occurs at group k, then the differences + # group[k+1] - group[k] and group[k] - group[k-1] are excluded. + d2use[oflags_locs[1:, :]==1] = 0 + d2use[oflags_locs[:-1, :]==1] = 0 + + return d2use + + +def inital_countrateguess(covar, diffs, diffs2use): """ Compute the initial count rate. Parameters ---------- - Cov : Covar + covar : Covar The class that computes and contains the covariance matrix info. diffs : ndarray @@ -119,30 +182,25 @@ def inital_countrateguess(Cov, diffs, diffs2use): diffs2use : ndarray Boolean mask determining with group differences to use (ngroups-1, ncols). + + Returns + ------- + countrateguess : ndarray + The initial count rate. """ # initial guess for count rate is the average of the unmasked # group differences unless otherwise specified. - if Cov.pedestal: + if covar.pedestal: num = np.sum((diffs * diffs2use)[1:], axis=0) den = np.sum(diffs2use[1:], axis=0) - countrateguess = num / den - ''' - countrateguess = np.sum((diffs * diffs2use)[1:], axis=0) / np.sum( - diffs2use[1:], axis=0 - ) - ''' else: num = np.sum((diffs * diffs2use), axis=0) den = np.sum(diffs2use, axis=0) - countrateguess = num / den - ''' - countrateguess = np.sum((diffs * diffs2use), axis=0) / np.sum( - diffs2use, axis=0 - ) - ''' + + countrateguess = num / den countrateguess *= countrateguess > 0 - return countrateguess + return countrateguess # RAMP FITTING BEGIN @@ -192,7 +250,7 @@ def fit_ramps( resetsig : float or ndarray Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. - Optional, default np.inf, i.e., reset values have flat priors. + Optional, default np.inf, i.e., reset values have flat priors. rescale : boolean Scale the covariance matrix internally to avoid possible @@ -204,11 +262,6 @@ def fit_ramps( result : Ramp_Result Holds computed ramp fitting information. XXX - rename """ - # XXX - # this needs to be refactored into a well written function, - # instead of meandering crap, hundreds of lines long. The - # next function is at line 544. - if diffs2use is None: # Use all diffs diffs2use = np.ones(diffs.shape, np.uint8) @@ -227,7 +280,7 @@ def fit_ramps( beta = beta * diffs2use[1:] * diffs2use[:-1] # All definitions and formulas here are in the paper. - # --- Till line 284: Paper 1 section 4 + # --- Till line 237: Paper 1 section 4 theta = compute_thetas(ndiffs, npix, alpha, beta) # EQNs 38-40 phi = compute_phis(ndiffs, npix, alpha, beta) # EQNs 41-43 @@ -240,11 +293,31 @@ def fit_ramps( ThetaD = compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask) # EQN 48 dB, dC, A, B, C = matrix_computations( - ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD) - - result = get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig) + ndiffs, + npix, + sgn, + diff_mask, + diffs2use, + beta, + phi, + Phi, + PhiD, + theta, + Theta, + ThetaD, + ) + + result = get_ramp_result( + dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig + ) # --- Beginning at line 250: Paper 1 section 4 + # ============================================================================= + # ============================================================================= + # ============================================================================= + # XXX Refactor the below section. In fact the whole thing should be moved to + # another function, which itself should be refactored. + # The code below computes the best chi squared, best-fit slope, # and its uncertainty leaving out each group difference in # turn. There are ndiffs possible differences that can be @@ -356,8 +429,11 @@ def fit_ramps( result.fill_masked_reads(diffs2use) return result + + # RAMP FITTING END + def compute_abs(countrateguess, rnoise, covar, rescale): """ Compute alpha, beta, and scale needed for ramp fit. @@ -408,6 +484,7 @@ def compute_abs(countrateguess, rnoise, covar, rescale): return alpha, beta, scale + def compute_thetas(ndiffs, npix, alpha, beta): """ EQNs 38-40 @@ -496,6 +573,7 @@ def compute_Phis(ndiffs, npix, beta, phi, sgn): Phi[i] = Phi[i + 1] * beta[i] + sgn[i + 1] * beta[i] * phi[i + 2] return Phi + def compute_PhiDs(ndiffs, npix, beta, phi, sgn, diff_mask): """ EQN 4, Paper II @@ -600,7 +678,8 @@ def compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask): def matrix_computations( - ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD): + ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD +): """ Computing matrix computations needed for ramp fitting. EQNs 61-63, 71, 75 @@ -646,15 +725,19 @@ def matrix_computations( Returns ------- dB : ndarray + Intermediate computation. dC : ndarray + Intermediate computation. A : ndarray + Intermediate computation. B : ndarray + Intermediate computation. C : ndarray - + Intermediate computation. """ beta_extended = np.ones((ndiffs, npix)) beta_extended[1:] = beta @@ -669,7 +752,9 @@ def matrix_computations( # {\cal A}, {\cal B}, {\cal C} in the paper # EQNs 61-63 - A = 2 * np.sum(diff_mask * sgn / theta[-1] * beta_extended * phi[1:] * ThetaD[:-1], axis=0) + A = 2 * np.sum( + diff_mask * sgn / theta[-1] * beta_extended * phi[1:] * ThetaD[:-1], axis=0 + ) A += np.sum(diff_mask**2 * theta[:-1] * phi[1:] / theta[ndiffs], axis=0) B = np.sum(diff_mask * dC, axis=0) @@ -678,14 +763,62 @@ def matrix_computations( return dB, dC, A, B, C -def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, Cov, resetval, resetsig): +def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig): + """ + Use intermediate computations to fit the ramp and save the results. + + Parameters + ---------- + dB : ndarray + Intermediate computation. + + dC : ndarray + Intermediate computation. + + A : ndarray + Intermediate computation. + + B : ndarray + Intermediate computation. + + C : ndarray + Intermediate computation. + + rescale : boolean + Scale the covariance matrix internally to avoid possible + overflow/underflow problems for long ramps. + Optional, default is True. + + phi : ndarray + Intermediate computation. + + theta : ndarray + Intermediate computation. + + covar : Covar + The class that computes and contains the covariance matrix info. + + resetval : float or ndarray + Priors on the reset values. Irrelevant unless pedestal is True. If an + ndarray, it has dimensions (ncols). + Opfional, default is 0. + + resetsig : float or ndarray + Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. + Optional, default np.inf, i.e., reset values have flat priors. + + Returns + ------- + result : Ramp_Result + The results of the ramp fitting for a given row of pixels in an integration. + """ result = Ramp_Result() # Finally, save the best-fit count rate, chi squared, uncertainty # in the count rate, and the weights used to combine the # groups. - if not Cov.pedestal: + if not covar.pedestal: result.countrate = B / C result.chisq = (A - B**2 / C) / scale result.uncert = np.sqrt(scale / C) @@ -695,7 +828,7 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, Cov, resetval, resetsig) # in the paper. else: - dt = Cov.mean_t[0] + dt = covar.mean_t[0] Cinv_11 = theta[0] * phi[1] / theta[ndiffs] # Calculate the pedestal and slope using the equations in the paper. @@ -723,6 +856,7 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, Cov, resetval, resetsig) ################################################################################ ################################## DEBUG ####################################### + def dbg_print_info(group_time, readtimes, data, diff): print(DELIM) print(f"group_time = {group_time}") diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index 6fb4fd2b..3dbabaab 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -93,6 +93,10 @@ def create_ramp_fit_class(model, algorithm, dqflags=None, suppress_one_group=Fal ramp_data.zeroframe = model.zeroframe ramp_data.algorithm = algorithm + + if hasattr(model.meta.exposure, "read_pattern"): + ramp_data.read_pattern = [list(reads) for reads in model.meta.exposure.read_pattern] + ramp_data.set_dqflags(dqflags) ramp_data.start_row = 0 ramp_data.num_rows = ramp_data.data.shape[2] diff --git a/src/stcal/ramp_fitting/ramp_fit_class.py b/src/stcal/ramp_fitting/ramp_fit_class.py index 05243e3e..e6c6fe3e 100644 --- a/src/stcal/ramp_fitting/ramp_fit_class.py +++ b/src/stcal/ramp_fitting/ramp_fit_class.py @@ -16,6 +16,7 @@ def __init__(self): # Meta information self.instrument_name = None + self.read_pattern = None self.frame_time = None self.group_time = None diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 69fac89f..039e9242 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -24,11 +24,11 @@ } GOOD = test_dq_flags["GOOD"] -DO_NOT_USE = test_dq_flags["DO_NOT_USE"] -JUMP_DET = test_dq_flags["JUMP_DET"] -SATURATED = test_dq_flags["SATURATED"] -NO_GAIN_VALUE = test_dq_flags["NO_GAIN_VALUE"] -UNRELIABLE_SLOPE = test_dq_flags["UNRELIABLE_SLOPE"] +DNU = test_dq_flags["DO_NOT_USE"] +JMP = test_dq_flags["JUMP_DET"] +SAT = test_dq_flags["SATURATED"] +NGV = test_dq_flags["NO_GAIN_VALUE"] +USLOPE = test_dq_flags["UNRELIABLE_SLOPE"] DELIM = "-" * 70 @@ -166,3 +166,71 @@ def test_basic_ramp(): tol = 1.e-5 diff = abs(data - check) assert diff < tol + + # Check against OLS. + ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data1.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + + data1 = cube1[0][0, 0, 0] + diff = abs(data - data1) + assert diff < tol + + +def flagged_ramp_data(): + nints, ngroups, nrows, ncols = 1, 20, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + ramp_data.data[0, 10:, 0, 0] += 150. # Add a jump. + + # Create segments in the ramp, including a jump and saturation at the end. + dq = np.array([GOOD] * ngroups) + dq[2] = DNU + dq[17:] = SAT + dq[10] = JMP + ramp_data.groupdq[0, :, 0, 0] = dq + + return ramp_data, gain2d, rnoise2d + + +def test_flagged_ramp(): + """ + Test flagged ramp. + """ + ramp_data, gain2d, rnoise2d = flagged_ramp_data() + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + data = cube[0][0, 0, 0] + + # Check against OLS. + ramp_data, gain2d, rnoise2d = flagged_ramp_data() + + save_opt, algo, ncores = False, "OLS", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + data1 = cube[0][0, 0, 0] + + tol = 1.e-5 + diff = abs(data - data1) + assert diff < tol From 08855038059922bd208a79bae6fb99d88b12a8ed Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Tue, 9 Apr 2024 15:51:49 -0400 Subject: [PATCH 06/63] Attempts at computing values for the ERR array. --- src/stcal/ramp_fitting/likely_algo_classes.py | 5 +++- src/stcal/ramp_fitting/likely_fit.py | 6 ++++- tests/test_ramp_fitting_likly_fit.py | 25 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index a3ccf3b8..cbf51173 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -53,7 +53,9 @@ def get_results(self, result, integ, row): The current row being operated on. """ self.data[integ, row, :] = result.countrate - self.err[integ, row, :] = result.chisq + # self.err[integ, row, :] = result.chisq + # self.err[integ, row, :] = result.uncert + self.err[integ, row, :] = result.stderr class ImageInfo: @@ -92,6 +94,7 @@ def __init__(self): Contains the ramp fitting results. """ self.countrate = None + self.stderr = None self.chisq = None self.uncert = None self.weights = None diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 4446348b..fc13949b 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -819,7 +819,10 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi # groups. if not covar.pedestal: - result.countrate = B / C + invC = 1 / C + # result.countrate = B / C + result.countrate = B * invC + result.stderr = np.sqrt(invC) result.chisq = (A - B**2 / C) / scale result.uncert = np.sqrt(scale / C) result.weights = dC / C @@ -839,6 +842,7 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi a = B / C - b * dC[0] / C / dt result.pedestal = b result.countrate = a + result.stderr = np.sqrt(C) result.chisq = A + a**2 * C + b**2 / dt**2 * Cinv_11 result.chisq += -2 * b / dt * dB[0] - 2 * a * B + 2 * a * b / dt * dC[0] result.chisq /= scale diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 039e9242..74c90b71 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -139,7 +139,8 @@ def create_blank_ramp_data(dims, var, tm): def test_basic_ramp(): """ - Test a basic ramp with a linear progression up the ramp. + Test a basic ramp with a linear progression up the ramp. Compare the + integration results from the LIKELY algorithm to the OLS algorithm. """ nints, ngroups, nrows, ncols = 1, 10, 1, 1 rnval, gval = 10.0, 5.0 @@ -151,6 +152,7 @@ def test_basic_ramp(): ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + # Create a simple linear ramp. ramp = np.array(list(range(ngroups))) * 20 + 10 ramp_data.data[0, :, 0, 0] = ramp @@ -210,7 +212,9 @@ def flagged_ramp_data(): def test_flagged_ramp(): """ - Test flagged ramp. + Test flagged ramp. The flags will cause segments, as well as ramp + truncation. Compare the integration results from the LIKELY algorithm + to the OLS algorithm. """ ramp_data, gain2d, rnoise2d = flagged_ramp_data() @@ -234,3 +238,20 @@ def test_flagged_ramp(): tol = 1.e-5 diff = abs(data - data1) assert diff < tol + +# ----------------------------------------------------------------- + + +def dbg_print_cube_pix(cube, pix): + # (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) + da, dq, vp, vr, er = cube + row, col = pix + print(" ") + print(DELIM) + print(f"Data = {da[:, row, col]}") + print(f"DQ = {dq[:, row, col]}") + print(f"VP = {vp[:, row, col]}") + print(f"VR = {vr[:, row, col]}") + print(f"ERR = {er[:, row, col]}") + print(DELIM) + From aa2d28a4fa9609be81b1c76b27f4eab2863af489 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 11 Apr 2024 08:50:02 -0400 Subject: [PATCH 07/63] Adding DQ flag computation for integrations. Adding testing to analyze differences between this algorithm and the OLS. --- src/stcal/ramp_fitting/likely_algo_classes.py | 13 ++-- src/stcal/ramp_fitting/likely_fit.py | 13 +++- tests/test_ramp_fitting_likly_fit.py | 71 ++++++++++++++++++- 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index cbf51173..f1c0b89c 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -24,7 +24,7 @@ def __init__(self, nints, nrows, ncols): dims = (nints, nrows, ncols) self.data = np.zeros(shape=dims, dtype=np.float32) - self.idq = np.zeros(shape=dims, dtype=np.uint32) + self.dq = np.zeros(shape=dims, dtype=np.uint32) self.var_poisson = np.zeros(shape=dims, dtype=np.float32) self.var_rnoise = np.zeros(shape=dims, dtype=np.float32) @@ -35,7 +35,7 @@ def prepare_info(self): """ Arrange output arrays as a tuple, which the ramp fit step expects. """ - return (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) + return (self.data, self.dq, self.var_poisson, self.var_rnoise, self.err) def get_results(self, result, integ, row): """ @@ -53,9 +53,7 @@ def get_results(self, result, integ, row): The current row being operated on. """ self.data[integ, row, :] = result.countrate - # self.err[integ, row, :] = result.chisq - # self.err[integ, row, :] = result.uncert - self.err[integ, row, :] = result.stderr + self.err[integ, row, :] = result.uncert class ImageInfo: @@ -74,7 +72,7 @@ def __init__(self, nrows, ncols): dims = (nrows, ncols) self.data = np.zeros(shape=dims, dtype=np.float32) - self.idq = np.zeros(shape=dims, dtype=np.uint32) + self.dq = np.zeros(shape=dims, dtype=np.uint32) self.var_poisson = np.zeros(shape=dims, dtype=np.float32) self.var_rnoise = np.zeros(shape=dims, dtype=np.float32) @@ -85,7 +83,7 @@ def prepare_info(self): """ Package the data to be returned from ramp fitting. """ - return (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) + return (self.data, self.dq, self.var_poisson, self.var_rnoise, self.err) class Ramp_Result: @@ -94,7 +92,6 @@ def __init__(self): Contains the ramp fitting results. """ self.countrate = None - self.stderr = None self.chisq = None self.uncert = None self.weights = None diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index fc13949b..85ae1299 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -93,9 +93,12 @@ def likely_ramp_fit( covar = Covar(readtimes) integ_class = IntegInfo(nints, nrows, ncols) + # image_class = ImageInfo(nrows, ncols) for integ in range(nints): data = ramp_data.data[integ, :, :, :] + gdq = ramp_data.groupdq[integ, :, :, :].copy() + pdq = ramp_data.pixeldq[:, :].copy() diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] for row in range(nrows): @@ -103,8 +106,16 @@ def likely_ramp_fit( result = fit_ramps(diff[:, row], covar, readnoise_2d[row], diffs2use=d2use) integ_class.get_results(result, integ, row) + pdq = utils.dq_compress_sect(ramp_data, integ, gdq, pdq) + integ_class.dq[integ, :, :] = pdq + + del gdq + integ_info = integ_class.prepare_info() + # XXX Need to combine integration info into image info. + # final_pixeldq = utils.dq_compress_final(integ_class.dq, ramp_data) + return image_info, integ_info, opt_info @@ -822,7 +833,6 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi invC = 1 / C # result.countrate = B / C result.countrate = B * invC - result.stderr = np.sqrt(invC) result.chisq = (A - B**2 / C) / scale result.uncert = np.sqrt(scale / C) result.weights = dC / C @@ -842,7 +852,6 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi a = B / C - b * dC[0] / C / dt result.pedestal = b result.countrate = a - result.stderr = np.sqrt(C) result.chisq = A + a**2 * C + b**2 / dt**2 * Cinv_11 result.chisq += -2 * b / dt * dB[0] - 2 * a * B + 2 * a * b / dt * dC[0] result.chisq /= scale diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 74c90b71..879eabca 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -224,6 +224,7 @@ def test_flagged_ramp(): ) data = cube[0][0, 0, 0] + dq = cube[1][0, 0, 0] # Check against OLS. ramp_data, gain2d, rnoise2d = flagged_ramp_data() @@ -233,20 +234,84 @@ def test_flagged_ramp(): ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - data1 = cube[0][0, 0, 0] + data_ols = cube[0][0, 0, 0] + dq_ols = cube[1][0, 0, 0] tol = 1.e-5 - diff = abs(data - data1) + diff = abs(data - data_ols) assert diff < tol + assert dq == dq_ols + + +def random_ramp_data(): + nints, ngroups, nrows, ncols = 1, 10, 1, 1 + rnval, gval = 10.0, 5.0 + # frame_time, nframes, groupgap = 10.736, 4, 1 + frame_time, nframes, groupgap = 1., 1, 0 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + ramp = np.array([153., 307., 457., 604., 1853., 2002., 2159., 2308., 2459., 2601.]) + ramp_data.data[0, :, 0, 0] = ramp + + # Create a jump. + dq = np.array([GOOD] * ngroups) + dq[4] = JMP + ramp_data.groupdq[0, :, 0, 0] = dq + + return ramp_data, gain2d, rnoise2d + + +def test_random_ramp(): + """ + Created a slope with a base slope of 150., with random Poisson noise with lambda + 5.0. At group 4 is a jump of 1100.0. + Compare the integration results from the LIKELY algorithm to the OLS algorithm. + """ + print(" ") # XXX + ramp_data, gain2d, rnoise2d = random_ramp_data() + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + # dbg_print_cube_pix(cube, (0, 0), "LIKELY:") + + data = cube[0][0, 0, 0] + dq = cube[1][0, 0, 0] + err = cube[-1][0, 0, 0] + + # Check against OLS. + ramp_data, gain2d, rnoise2d = random_ramp_data() + + save_opt, algo, ncores = False, "OLS", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + # dbg_print_cube_pix(cube, (0, 0), "OLS:") + + data_ols = cube[0][0, 0, 0] + dq_ols = cube[1][0, 0, 0] + err_ols = cube[-1][0, 0, 0] + # ----------------------------------------------------------------- -def dbg_print_cube_pix(cube, pix): +def dbg_print_cube_pix(cube, pix, label=None): # (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) da, dq, vp, vr, er = cube row, col = pix print(" ") + if label is not None: + print(DELIM) + print(label) print(DELIM) print(f"Data = {da[:, row, col]}") print(f"DQ = {dq[:, row, col]}") From 130e42648320f8af5f060e140a4d2eb59407e280 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 24 Apr 2024 10:34:52 -0400 Subject: [PATCH 08/63] Began updating code to include the Poisson and read noise variance computation. --- src/stcal/ramp_fitting/likely_algo_classes.py | 2 ++ src/stcal/ramp_fitting/likely_fit.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index f1c0b89c..8cb73f5c 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -94,6 +94,8 @@ def __init__(self): self.countrate = None self.chisq = None self.uncert = None + self.var_poisson = None + self.var_rdnoise = None self.weights = None self.pedestal = None self.uncert_pedestal = None diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 85ae1299..c6ff56e9 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -214,6 +214,19 @@ def inital_countrateguess(covar, diffs, diffs2use): return countrateguess +''' +def fit_ramps( + diffs, + Cov, + sig, + countrateguess=None, + diffs2use=None, + detect_jumps=False, + resetval=0, + resetsig=np.inf, + rescale=True, + dn_scale=10): +''' # RAMP FITTING BEGIN def fit_ramps( diffs, @@ -837,6 +850,15 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi result.uncert = np.sqrt(scale / C) result.weights = dC / C + result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) + result.var_poisson += 2 * np.sum( + result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0) + + ''' + result.var_rdnoise = np.sum(result.weights**2*alpha_readnoise, axis=0) + result.var_rdnoise += 2*np.sum(result.weights[1:]*result.weights[:-1]*beta_readnoise, axis=0) + ''' + # If we are computing the pedestal, then we use the other formulas # in the paper. From b0a7da775f019654ba5c620cb5f9bcfb803650cc Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 26 Apr 2024 10:40:14 -0400 Subject: [PATCH 09/63] Updating the likelihood algorithm to compute the Poisson and read noise variance. --- src/stcal/ramp_fitting/likely_algo_classes.py | 4 +- src/stcal/ramp_fitting/likely_fit.py | 89 +++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 8cb73f5c..25d44ad0 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -54,6 +54,8 @@ def get_results(self, result, integ, row): """ self.data[integ, row, :] = result.countrate self.err[integ, row, :] = result.uncert + self.var_poisson[integ, row, :] = result.var_poisson + self.var_rnoise[integ, row, :] = result.var_rnoise class ImageInfo: @@ -95,7 +97,7 @@ def __init__(self): self.chisq = None self.uncert = None self.var_poisson = None - self.var_rdnoise = None + self.var_rnoise = None self.weights = None self.pedestal = None self.uncert_pedestal = None diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index c6ff56e9..2bd2221c 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -238,6 +238,7 @@ def fit_ramps( resetval=0, resetsig=np.inf, rescale=True, + dn_scale=10., ): """ Function fit_ramps on a row of pixels. Fits ramps to read differences @@ -281,6 +282,9 @@ def fit_ramps( overflow/underflow problems for long ramps. Optional, default is True. + dn_scale : XXX + XXX + Returns ------- result : Ramp_Result @@ -294,8 +298,12 @@ def fit_ramps( if countrateguess is None: countrateguess = inital_countrateguess(covar, diffs, diffs2use) - alpha, beta, scale = compute_abs(countrateguess, rnoise, covar, rescale) - ndiffs, npix = alpha.shape + alpha_tuple, beta_tuple, scale = compute_abs( + countrateguess, rnoise, covar, rescale, diffs, dn_scale) + alpha, alpha_phnoise, alpha_readnoise = alpha_tuple + beta, beta_phnoise, beta_readnoise = beta_tuple + + ndiffs, npix = diffs.shape # Mask group differences that should be ignored. This is half # of what we need to do to mask these group differences; the @@ -332,7 +340,8 @@ def fit_ramps( ) result = get_ramp_result( - dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig + dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig, + alpha_phnoise, alpha_readnoise, beta_phnoise, beta_readnoise ) # --- Beginning at line 250: Paper 1 section 4 @@ -458,7 +467,7 @@ def fit_ramps( # RAMP FITTING END -def compute_abs(countrateguess, rnoise, covar, rescale): +def compute_abs(countrateguess, rnoise, covar, rescale, diffs, dn_scale): """ Compute alpha, beta, and scale needed for ramp fit. Elements of the covariance matrix. @@ -478,6 +487,12 @@ def compute_abs(countrateguess, rnoise, covar, rescale): rescale : bool Determination to rescale covariance matrix. + diffs : ndarray + The group differences of the data (ngroups-1, nrows, ncols). + + dn_scale : XXX + XXX + Returns ------- alpha : ndarray @@ -490,23 +505,57 @@ def compute_abs(countrateguess, rnoise, covar, rescale): Overflow/underflow prevention scale. """ - alpha = countrateguess * covar.alpha_phnoise[:, np.newaxis] - alpha += rnoise**2 * covar.alpha_readnoise[:, np.newaxis] - beta = countrateguess * covar.beta_phnoise[:, np.newaxis] - beta += rnoise**2 * covar.beta_readnoise[:, np.newaxis] + alpha_phnoise = countrateguess * covar.alpha_phnoise[:, np.newaxis] + alpha_readnoise = rnoise**2 * covar.alpha_readnoise[:, np.newaxis] + alpha = alpha_phnoise + alpha_readnoise - # rescale the covariance matrix to a determinant of order 1 to + beta_phnoise = countrateguess * covar.beta_phnoise[:, np.newaxis] + beta_readnoise = rnoise**2 * covar.beta_readnoise[:, np.newaxis] + beta = beta_phnoise + beta_readnoise + + ndiffs, npix = diffs.shape + + # Rescale the covariance matrix to a determinant of 1 to # avoid possible overflow/underflow. The uncertainty and chi - # squared value will need to be scaled back later. + # squared value will need to be scaled back later. Note that + # theta[-1] is the determinant of the covariance matrix. + # + # The method below uses the fact that if all alpha and beta + # are multiplied by f, theta[i] is multiplied by f**i. Keep + # a running track of these factors to construct the scale at + # the end, and keep scaling throughout so that we never risk + # overflow or underflow. + if rescale: - scale = np.exp(np.mean(np.log(alpha), axis=0)) + # scale = np.exp(np.mean(np.log(alpha), axis=0)) + theta = np.ones((ndiffs + 1, npix)) + theta[1] = alpha[0] + + scale = theta[0] * 1 + for i in range(2, ndiffs + 1): + theta[i] = alpha[i-1] / scale * theta[i-1] - beta[i-2]**2 / scale**2 * theta[i-2] + + # Scaling every ten steps in safe for alpha up to 1e20 + # or so and incurs a negligible computational cost for + # the fractional power. + + if i % int(dn_scale) == 0 or i == ndiffs: + f = theta[i]**(1/i) + scale *= f + tmp = theta[i] / f + theta[i-1] /= tmp + theta[i-2] /= (tmp / f) + theta[i] = 1 else: scale = 1 alpha /= scale beta /= scale - return alpha, beta, scale + alpha_tuple = (alpha, alpha_phnoise, alpha_readnoise) + beta_tuple = (beta, beta_phnoise, beta_readnoise) + + return alpha_tuple, beta_tuple, scale def compute_thetas(ndiffs, npix, alpha, beta): @@ -787,7 +836,9 @@ def matrix_computations( return dB, dC, A, B, C -def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig): +def get_ramp_result( + dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig, + alpha_phnoise, alpha_readnoise, beta_phnoise, beta_readnoise): """ Use intermediate computations to fit the ramp and save the results. @@ -831,6 +882,11 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. Optional, default np.inf, i.e., reset values have flat priors. + alpha_phnoise : + alpha_readnoise : + beta_phnoise : + beta_readnoise : + Returns ------- result : Ramp_Result @@ -854,10 +910,9 @@ def get_ramp_result(dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsi result.var_poisson += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0) - ''' - result.var_rdnoise = np.sum(result.weights**2*alpha_readnoise, axis=0) - result.var_rdnoise += 2*np.sum(result.weights[1:]*result.weights[:-1]*beta_readnoise, axis=0) - ''' + result.var_rdnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) + result.var_rdnoise += 2 * np.sum( + result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0) # If we are computing the pedestal, then we use the other formulas # in the paper. From dbe44c7bf66aced079bcf2de439dd3c1f556eab0 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 1 May 2024 06:55:05 -0400 Subject: [PATCH 10/63] Update tests and debugging functions. --- tests/test_ramp_fitting_likly_fit.py | 40 +++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 879eabca..859e4f91 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -184,6 +184,8 @@ def test_basic_ramp(): diff = abs(data - data1) assert diff < tol + dbg_print_cubel_cube1(cube, cube1) + def flagged_ramp_data(): nints, ngroups, nrows, ncols = 1, 20, 1, 1 @@ -230,18 +232,20 @@ def test_flagged_ramp(): ramp_data, gain2d, rnoise2d = flagged_ramp_data() save_opt, algo, ncores = False, "OLS", "none" - slopes, cube, ols_opt, gls_opt = ramp_fit_data( + slopes, cube1, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - data_ols = cube[0][0, 0, 0] - dq_ols = cube[1][0, 0, 0] + data_ols = cube1[0][0, 0, 0] + dq_ols = cube1[1][0, 0, 0] tol = 1.e-5 diff = abs(data - data_ols) assert diff < tol assert dq == dq_ols + dbg_print_cubel_cube1(cube, cube1) + def random_ramp_data(): nints, ngroups, nrows, ncols = 1, 10, 1, 1 @@ -266,6 +270,7 @@ def random_ramp_data(): return ramp_data, gain2d, rnoise2d +@pytest.mark.skip(reason="Not sure what expected value is.") def test_random_ramp(): """ Created a slope with a base slope of 150., with random Poisson noise with lambda @@ -290,18 +295,41 @@ def test_random_ramp(): ramp_data, gain2d, rnoise2d = random_ramp_data() save_opt, algo, ncores = False, "OLS", "none" - slopes, cube, ols_opt, gls_opt = ramp_fit_data( + slopes, cube1, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - # dbg_print_cube_pix(cube, (0, 0), "OLS:") - data_ols = cube[0][0, 0, 0] dq_ols = cube[1][0, 0, 0] err_ols = cube[-1][0, 0, 0] + dbg_print_cubel_cube1(cube, cube1) + # ----------------------------------------------------------------- +def dbg_print_cubel_cube1(cube, cube1): + print(" ") + print(DELIM) + d_l = cube[0][0, 0, 0] + d_o = cube1[0][0, 0, 0] + print(f"data LIK = {d_l}") + print(f"data OLS = {d_o}\n") + + vp_l = cube[2][0, 0, 0] + vp_o = cube1[2][0, 0, 0] + print(f"var_poisson LIK = {vp_l}") + print(f"var_poisson OLS = {vp_o}\n") + + vr_l = cube[3][0, 0, 0] + vr_o = cube1[3][0, 0, 0] + print(f"var_rnoise LIK = {vr_l}") + print(f"var_rnoise OLS = {vr_o}\n") + + er_l = cube[4][0, 0, 0] + er_o = cube1[4][0, 0, 0] + print(f"err LIK = {er_l}") + print(f"err OLS = {er_o}") + print(DELIM) def dbg_print_cube_pix(cube, pix, label=None): From fe54daba428a6378e9e8ca3502afa6f20041572c Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Tue, 7 May 2024 08:05:40 -0400 Subject: [PATCH 11/63] Adding rate product computations. --- src/stcal/ramp_fitting/likely_algo_classes.py | 30 ----- src/stcal/ramp_fitting/likely_fit.py | 67 ++++++++-- tests/test_ramp_fitting_likly_fit.py | 114 ++++++++++-------- 3 files changed, 120 insertions(+), 91 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 25d44ad0..4892bb7f 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -58,36 +58,6 @@ def get_results(self, result, integ, row): self.var_rnoise[integ, row, :] = result.var_rnoise -class ImageInfo: - def __init__(self, nrows, ncols): - """ - Storage for the observation information for ramp fitting computations. - - Parameters - ---------- - nrows : int - The number of rows in the data. - - ncols : int - The number of columns in the data. - """ - dims = (nrows, ncols) - self.data = np.zeros(shape=dims, dtype=np.float32) - - self.dq = np.zeros(shape=dims, dtype=np.uint32) - - self.var_poisson = np.zeros(shape=dims, dtype=np.float32) - self.var_rnoise = np.zeros(shape=dims, dtype=np.float32) - - self.err = np.zeros(shape=dims, dtype=np.float32) - - def prepare_info(self): - """ - Package the data to be returned from ramp fitting. - """ - return (self.data, self.dq, self.var_poisson, self.var_rnoise, self.err) - - class Ramp_Result: def __init__(self): """ diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2bd2221c..e1dfdd45 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -9,17 +9,10 @@ import numpy as np from . import ramp_fit_class, utils -from .likely_algo_classes import IntegInfo, ImageInfo, Ramp_Result, Covar +from .likely_algo_classes import IntegInfo, Ramp_Result, Covar -################## DEBUG ################## -# HELP!! -import ipdb -import sys -sys.path.insert(1, "/Users/kmacdonald/code/common") -from general_funcs import DELIM, dbg_print, array_string - -################## DEBUG ################## +DELIM = '=' * 80 log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -93,7 +86,6 @@ def likely_ramp_fit( covar = Covar(readtimes) integ_class = IntegInfo(nints, nrows, ncols) - # image_class = ImageInfo(nrows, ncols) for integ in range(nints): data = ramp_data.data[integ, :, :, :] @@ -112,13 +104,58 @@ def likely_ramp_fit( del gdq integ_info = integ_class.prepare_info() - - # XXX Need to combine integration info into image info. - # final_pixeldq = utils.dq_compress_final(integ_class.dq, ramp_data) + image_info = compute_image_info(integ_class, ramp_data) return image_info, integ_info, opt_info +def compute_image_info(integ_class, ramp_data): + """ + Compute the diffs2use mask based on DQ flags of a row. + + Parameters + ---------- + integ_class : IntegInfo + Contains the rateints product calculations. + + ramp_data : RampData + Input data necessary for computing ramp fitting. + + Returns + ------- + image_info : tuple + The list of arrays for the rate product. + """ + if integ_class.data.shape[0] == 1: + data = integ_class.data[0, :, :] + dq = integ_class.dq[0, :, :] + var_p = integ_class.var_poisson[0, :, :] + var_r = integ_class.var_rnoise[0, :, :] + var_e = integ_class.err[0, :, :] + return (data, dq, var_p, var_r, var_e) + + dq = utils.dq_compress_final(integ_class.dq, ramp_data) + + inv_vp = 1. / integ_class.var_poisson + var_p = 1. / inv_vp.sum(axis=0) + + inv_vr = 1. / integ_class.var_rnoise + var_r = 1. / inv_vr.sum(axis=0) + + inv_err = 1. / integ_class.err + err = 1. / inv_err.sum(axis=0) + + inv_err2 = 1. / (integ_class.err**2) + err2 = 1. / inv_err2.sum(axis=0) + + slope = integ_class.data * inv_err2 + slope = slope.sum(axis=0) * err2 + + # Compute NaNs. + + return (slope, dq, var_p, var_r, err) + + def determine_diffs2use(ramp_data, integ, row, diffs): """ Compute the diffs2use mask based on DQ flags of a row. @@ -960,3 +997,7 @@ def dbg_print_info(group_time, readtimes, data, diff): print(DELIM) print(f"diff = {array_string(diff[:, 0, 0])}") print(DELIM) + + +def array_string(arr, prec=4): + return np.array2string(arr, precision=prec, max_line_width=np.nan, separator=", ") diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 859e4f91..a5ef8e7d 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -4,15 +4,6 @@ from stcal.ramp_fitting.ramp_fit import ramp_fit_class, ramp_fit_data from stcal.ramp_fitting.ramp_fit_class import RampData -################## DEBUG ################## -# HELP!! -import ipdb -import sys - -sys.path.insert(1, "/Users/kmacdonald/code/common") -from general_funcs import DELIM, dbg_print, array_string - -################## DEBUG ################## test_dq_flags = { "GOOD": 0, @@ -184,7 +175,49 @@ def test_basic_ramp(): diff = abs(data - data1) assert diff < tol - dbg_print_cubel_cube1(cube, cube1) + +def test_basic_ramp_2integ(): + """ + Test a basic ramp with a linear progression up the ramp. Compare the + integration results from the LIKELY algorithm to the OLS algorithm. + """ + nints, ngroups, nrows, ncols = 2, 10, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + ramp_data.data[1, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + tol = 1.e-5 + + # Check against OLS. + ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data1.data[0, :, 0, 0] = ramp + ramp_data1.data[1, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + + # dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + # dbg_print_cube_cube1(cube, cube1, (0, 0)) + def flagged_ramp_data(): @@ -244,8 +277,6 @@ def test_flagged_ramp(): assert diff < tol assert dq == dq_ols - dbg_print_cubel_cube1(cube, cube1) - def random_ramp_data(): nints, ngroups, nrows, ncols = 1, 10, 1, 1 @@ -285,8 +316,6 @@ def test_random_ramp(): ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - # dbg_print_cube_pix(cube, (0, 0), "LIKELY:") - data = cube[0][0, 0, 0] dq = cube[1][0, 0, 0] err = cube[-1][0, 0, 0] @@ -303,48 +332,37 @@ def test_random_ramp(): dq_ols = cube[1][0, 0, 0] err_ols = cube[-1][0, 0, 0] - dbg_print_cubel_cube1(cube, cube1) - # ----------------------------------------------------------------- -def dbg_print_cubel_cube1(cube, cube1): +def dbg_print_slope_slope1(slope, slope1, pix): + data, dq, vp, vr, err = slope + data1, dq1, vp1, vr1, err1 = slope1 + row, col = pix + print(" ") print(DELIM) - d_l = cube[0][0, 0, 0] - d_o = cube1[0][0, 0, 0] - print(f"data LIK = {d_l}") - print(f"data OLS = {d_o}\n") - - vp_l = cube[2][0, 0, 0] - vp_o = cube1[2][0, 0, 0] - print(f"var_poisson LIK = {vp_l}") - print(f"var_poisson OLS = {vp_o}\n") - - vr_l = cube[3][0, 0, 0] - vr_o = cube1[3][0, 0, 0] - print(f"var_rnoise LIK = {vr_l}") - print(f"var_rnoise OLS = {vr_o}\n") - - er_l = cube[4][0, 0, 0] - er_o = cube1[4][0, 0, 0] - print(f"err LIK = {er_l}") - print(f"err OLS = {er_o}") + print("Slope Information:") + print(f" Pixel = ({row}, {col})") + + print(f"data LIK = {data[row, col]}") + print(f"data OLS = {data1[row, col]}") + print(DELIM) -def dbg_print_cube_pix(cube, pix, label=None): - # (self.data, self.idq, self.var_poisson, self.var_rnoise, self.err) - da, dq, vp, vr, er = cube +def dbg_print_cube_cube1(cube, cube1, pix): + data, dq, vp, vr, err = cube + data1, dq1, vp1, vr1, err1 = cube1 row, col = pix + nints = data1.shape[0] + print(" ") - if label is not None: - print(DELIM) - print(label) - print(DELIM) - print(f"Data = {da[:, row, col]}") - print(f"DQ = {dq[:, row, col]}") - print(f"VP = {vp[:, row, col]}") - print(f"VR = {vr[:, row, col]}") - print(f"ERR = {er[:, row, col]}") print(DELIM) + print("Cube Information:") + print(f" Pixel = ({row}, {col})") + print(f" Number of Integrations = {nints}") + + print(f"data LIK = {data[:, row, col]}") + print(f"data OLS = {data1[:, row, col]}") + print(DELIM) From 6c497fb8bed732e4e24a9bc40165516386f088fa Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Sat, 11 May 2024 16:26:59 -0400 Subject: [PATCH 12/63] Updating likelihood testing. --- tests/test_ramp_fitting_likly_fit.py | 129 ++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index a5ef8e7d..0da7f97e 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -304,9 +304,10 @@ def random_ramp_data(): @pytest.mark.skip(reason="Not sure what expected value is.") def test_random_ramp(): """ - Created a slope with a base slope of 150., with random Poisson noise with lambda - 5.0. At group 4 is a jump of 1100.0. - Compare the integration results from the LIKELY algorithm to the OLS algorithm. + Created a slope with a base slope of 150., with random Poisson + noise with lambda 5.0. At group 4 is a jump of 1100.0. + Compare the integration results from the LIKELY algorithm + to the OLS algorithm. """ print(" ") # XXX ramp_data, gain2d, rnoise2d = random_ramp_data() @@ -333,6 +334,128 @@ def test_random_ramp(): err_ols = cube[-1][0, 0, 0] +def test_long_ramp(): + nints, ngroups, nrows, ncols = 1, 200, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + data = cube[0][0, 0, 0] + ddiff = (ramp_data.data[0, ngroups-1, 0, 0] - ramp_data.data[0, 0, 0, 0]) + check = ddiff / float(ngroups-1) + check = check / ramp_data.group_time + tol = 1.e-5 + diff = abs(data - check) + assert diff < tol + + # Check against OLS. + ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data1.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + + data1 = cube1[0][0, 0, 0] + diff = abs(data - data1) + assert diff < tol + + +@pytest.mark.parametrize("ngroups", [3, 2]) +@pytest.mark.parametrize("nframes", [1, 2, 4, 8]) +def test_short_integrations(ngroups, nframes): + """ + Check short 3 and 2 group integrations. + """ + nints, nrows, ncols = 1, 1, 1 + rnval, gval = 10.0, 5.0 + # frame_time, nframes, groupgap = 10.736, 4, 1 + frame_time, groupgap = 10.736, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + data = cube[0][0, 0, 0] + ddiff = (ramp_data.data[0, ngroups-1, 0, 0] - ramp_data.data[0, 0, 0, 0]) + check = ddiff / float(ngroups-1) + check = check / ramp_data.group_time + tol = 1.e-5 + diff = abs(data - check) + assert diff < tol + + # Check against OLS. + ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) + + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data1.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + + data1 = cube1[0][0, 0, 0] + diff = abs(data - data1) + assert diff < tol + + +def test_1group(): + """ + The number of groups must be greater than 1, so make sure an + exception is raised where ngroups == 1. + """ + nints, ngroups, nrows, ncols = 1, 1, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + with pytest.raises(ValueError): + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, + "optimal", ncores, test_dq_flags + ) + + +# ----------------------------------------------------------------- # ----------------------------------------------------------------- def dbg_print_slope_slope1(slope, slope1, pix): data, dq, vp, vr, err = slope From e2af635d010ef1daab0c28baf8bc7a0e484a4bd5 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Sat, 11 May 2024 16:28:33 -0400 Subject: [PATCH 13/63] Correcting the computation of alpha_readnoise. --- src/stcal/ramp_fitting/likely_algo_classes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 4892bb7f..d9f67664 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -234,7 +234,8 @@ def _compute_means_and_taus(self, readtimes, pedestal): tau += [times] N += [1] - # readtimes is a list of lists, so mean_t is the list of each mean of each list. + # readtimes is a list of lists, so mean_t is the list of each + # mean of each list. mean_t = np.array(mean_t) tau = np.array(tau) N = np.array(N) @@ -260,7 +261,7 @@ def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): delta_t : ndarray The group differences of integration ramps. """ - self.alpha_readnoise = 1 / N[:-1] + 1 / (N[1:]) * delta_t**2 + self.alpha_readnoise = (1 / N[:-1] + 1 / N[1:]) / delta_t**2 self.beta_readnoise = -1 / (N[1:-1] * delta_t[1:] * delta_t[:-1]) self.alpha_phnoise = (tau[:-1] + tau[1:] - 2 * mean_t[:-1]) / delta_t**2 From 6ee83374656b595f775684a4b8c9e113c1a15d51 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Sat, 11 May 2024 16:30:50 -0400 Subject: [PATCH 14/63] Making some style changes and adding an exception if the ngroups are less than 2 groups, since the likelihood algorithm requires at least 2 groups per integration. --- src/stcal/ramp_fitting/likely_fit.py | 40 +++++++++------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index e1dfdd45..04ef3986 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -78,13 +78,19 @@ def likely_ramp_fit( nints, ngroups, nrows, ncols = ramp_data.data.shape + if ngroups < 2: + raise ValueError( + "Likelihood fit requires at least 2 groups." + ) + if ramp_data.read_pattern is None: # XXX Not sure if this is the right way to do things. readtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] + # readtimes = [(k + 1) for k in range(ngroups)] else: readtimes = read_data.read_pattern - covar = Covar(readtimes) + covar = Covar(readtimes) # XXX Choice of pedestal not given integ_class = IntegInfo(nints, nrows, ncols) for integ in range(nints): @@ -251,19 +257,6 @@ def inital_countrateguess(covar, diffs, diffs2use): return countrateguess -''' -def fit_ramps( - diffs, - Cov, - sig, - countrateguess=None, - diffs2use=None, - detect_jumps=False, - resetval=0, - resetsig=np.inf, - rescale=True, - dn_scale=10): -''' # RAMP FITTING BEGIN def fit_ramps( diffs, @@ -362,18 +355,8 @@ def fit_ramps( ThetaD = compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask) # EQN 48 dB, dC, A, B, C = matrix_computations( - ndiffs, - npix, - sgn, - diff_mask, - diffs2use, - beta, - phi, - Phi, - PhiD, - theta, - Theta, - ThetaD, + ndiffs, npix, sgn, diff_mask, diffs2use, beta, + phi, Phi, PhiD, theta, Theta, ThetaD, ) result = get_ramp_result( @@ -935,6 +918,7 @@ def get_ramp_result( # in the count rate, and the weights used to combine the # groups. + # XXX pedestal is always False. if not covar.pedestal: invC = 1 / C # result.countrate = B / C @@ -947,8 +931,8 @@ def get_ramp_result( result.var_poisson += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0) - result.var_rdnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) - result.var_rdnoise += 2 * np.sum( + result.var_rnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) + result.var_rnoise += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0) # If we are computing the pedestal, then we use the other formulas From 0ca17774759d9f5a4d8a024de6277dd469bc19aa Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 15 May 2024 07:11:19 -0400 Subject: [PATCH 15/63] Adding gain to the computatios. --- src/stcal/ramp_fitting/likely_fit.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 04ef3986..9e141b22 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -101,7 +101,7 @@ def likely_ramp_fit( for row in range(nrows): d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) - result = fit_ramps(diff[:, row], covar, readnoise_2d[row], diffs2use=d2use) + result = fit_ramps(diff[:, row], covar, gain_2d[row], readnoise_2d[row], diffs2use=d2use) integ_class.get_results(result, integ, row) pdq = utils.dq_compress_sect(ramp_data, integ, gdq, pdq) @@ -261,6 +261,7 @@ def inital_countrateguess(covar, diffs, diffs2use): def fit_ramps( diffs, covar, + gain, rnoise, countrateguess=None, diffs2use=None, @@ -283,8 +284,11 @@ def fit_ramps( covar : Covar The class that computes and contains the covariance matrix info. + gain : ndarray + The gain (ncols,) + rnoise : ndarray - The read noise (ncols,). XXX - the name should be changed. + The read noise (ncols,) countrateguess : ndarray Count rate estimates used to estimate the covariance matrix. @@ -329,7 +333,7 @@ def fit_ramps( countrateguess = inital_countrateguess(covar, diffs, diffs2use) alpha_tuple, beta_tuple, scale = compute_abs( - countrateguess, rnoise, covar, rescale, diffs, dn_scale) + countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale) alpha, alpha_phnoise, alpha_readnoise = alpha_tuple beta, beta_phnoise, beta_readnoise = beta_tuple @@ -487,7 +491,7 @@ def fit_ramps( # RAMP FITTING END -def compute_abs(countrateguess, rnoise, covar, rescale, diffs, dn_scale): +def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): """ Compute alpha, beta, and scale needed for ramp fit. Elements of the covariance matrix. @@ -498,8 +502,11 @@ def compute_abs(countrateguess, rnoise, covar, rescale, diffs, dn_scale): countrateguess : ndarray Initial guess (ncols,) + gain : ndarray + Gain (ncols,) + rnoise : ndarray - Readnoise (ncols,) + Read noise (ncols,) covar : Covar The class that computes and contains the covariance matrix info. @@ -525,11 +532,11 @@ def compute_abs(countrateguess, rnoise, covar, rescale, diffs, dn_scale): Overflow/underflow prevention scale. """ - alpha_phnoise = countrateguess * covar.alpha_phnoise[:, np.newaxis] + alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] alpha_readnoise = rnoise**2 * covar.alpha_readnoise[:, np.newaxis] alpha = alpha_phnoise + alpha_readnoise - beta_phnoise = countrateguess * covar.beta_phnoise[:, np.newaxis] + beta_phnoise = countrateguess / gain * covar.beta_phnoise[:, np.newaxis] beta_readnoise = rnoise**2 * covar.beta_readnoise[:, np.newaxis] beta = beta_phnoise + beta_readnoise @@ -553,7 +560,8 @@ def compute_abs(countrateguess, rnoise, covar, rescale, diffs, dn_scale): scale = theta[0] * 1 for i in range(2, ndiffs + 1): - theta[i] = alpha[i-1] / scale * theta[i-1] - beta[i-2]**2 / scale**2 * theta[i-2] + theta[i] = alpha[i-1] / scale * theta[i-1] \ + - beta[i-2]**2 / scale**2 * theta[i-2] # Scaling every ten steps in safe for alpha up to 1e20 # or so and incurs a negligible computational cost for From 4572beff7926667cc076480c226019dbf9a94850 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 15 May 2024 12:14:02 -0400 Subject: [PATCH 16/63] Changes during testing of readtime computations. --- src/stcal/ramp_fitting/likely_fit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 9e141b22..b05bd609 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -85,6 +85,7 @@ def likely_ramp_fit( if ramp_data.read_pattern is None: # XXX Not sure if this is the right way to do things. + # The group time maybe should be used at the end, rather than here. readtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] # readtimes = [(k + 1) for k in range(ngroups)] else: From 239349c356bc075f4b38aec530934dd4d89a0703 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 15 May 2024 12:14:43 -0400 Subject: [PATCH 17/63] Updating testing and debugging functions for testing. --- tests/test_ramp_fitting_likly_fit.py | 124 +++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 0da7f97e..40bbc142 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -160,6 +160,7 @@ def test_basic_ramp(): diff = abs(data - check) assert diff < tol + # Check against OLS. ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) @@ -201,8 +202,6 @@ def test_basic_ramp_2integ(): ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - tol = 1.e-5 - # Check against OLS. ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) @@ -215,9 +214,11 @@ def test_basic_ramp_2integ(): ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags ) - # dbg_print_slope_slope1(slopes, slopes1, (0, 0)) - # dbg_print_cube_cube1(cube, cube1, (0, 0)) - + tol = 1.e-5 + data = cube[0][0, 0, 0] + data1 = cube1[0][0, 0, 0] + diff = abs(data - data1) + assert diff < tol def flagged_ramp_data(): @@ -281,8 +282,8 @@ def test_flagged_ramp(): def random_ramp_data(): nints, ngroups, nrows, ncols = 1, 10, 1, 1 rnval, gval = 10.0, 5.0 - # frame_time, nframes, groupgap = 10.736, 4, 1 - frame_time, nframes, groupgap = 1., 1, 0 + frame_time, nframes, groupgap = 10.736, 5, 2 + # frame_time, nframes, groupgap = 1., 1, 0 dims = nints, ngroups, nrows, ncols var = rnval, gval @@ -290,6 +291,9 @@ def random_ramp_data(): ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + # A randomly generated ramp by setting up a ramp that has a slope of 150. + # with some randomly added Poisson values, with lambda=5., and a jump + # at group 4. ramp = np.array([153., 307., 457., 604., 1853., 2002., 2159., 2308., 2459., 2601.]) ramp_data.data[0, :, 0, 0] = ramp @@ -309,8 +313,8 @@ def test_random_ramp(): Compare the integration results from the LIKELY algorithm to the OLS algorithm. """ - print(" ") # XXX ramp_data, gain2d, rnoise2d = random_ramp_data() + dbg_print_basic_ramp(ramp_data) save_opt, algo, ncores = False, "LIKELY", "none" slopes, cube, ols_opt, gls_opt = ramp_fit_data( @@ -325,13 +329,16 @@ def test_random_ramp(): ramp_data, gain2d, rnoise2d = random_ramp_data() save_opt, algo, ncores = False, "OLS", "none" - slopes, cube1, ols_opt, gls_opt = ramp_fit_data( + slopes1, cube1, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - data_ols = cube[0][0, 0, 0] - dq_ols = cube[1][0, 0, 0] - err_ols = cube[-1][0, 0, 0] + data_ols = cube1[0][0, 0, 0] + dq_ols = cube1[1][0, 0, 0] + err_ols = cube1[-1][0, 0, 0] + + ddiff = abs(data - data_ols) + # XXX Finish def test_long_ramp(): @@ -456,10 +463,77 @@ def test_1group(): # ----------------------------------------------------------------- +# DEBUG # ----------------------------------------------------------------- -def dbg_print_slope_slope1(slope, slope1, pix): +def dbg_print_basic_ramp(ramp_data, pix=(0, 0)): + row, col = pix + nints = ramp_data.data.shape[0] + data = ramp_data.data[:, :, row, col] + dq = ramp_data.groupdq[:, :, row, col] + + print(" ") + print(DELIM) + print(f"Data Shape: {ramp_data.data.shape}") + print(DELIM) + print("Data:") + for integ in range(nints): + arr_str = np.array2string(data[integ, :], max_line_width=np.nan, separator=", ") + print(f"[{integ}] {arr_str}") + print(DELIM) + + print("DQ:") + for integ in range(nints): + arr_str = np.array2string(dq[integ, :], max_line_width=np.nan, separator=", ") + print(f"[{integ}] {arr_str}") + print(DELIM) + + +def dbg_print_slopes(slope, pix=(0, 0), label=None): data, dq, vp, vr, err = slope - data1, dq1, vp1, vr1, err1 = slope1 + row, col = pix + + print(" ") + print(DELIM) + if label is not None: + print("Slope Information: ({label})") + else: + print("Slope Information:") + print(f" Pixel = ({row}, {col})") + + print(f"data = {data[row, col]}") + print(f"dq = {dq[row, col]}") + print(f"vp = {vp[row, col]}") + print(f"vr = {vr[row, col]}\n") + + print(DELIM) + + +def dbg_print_cube(cube, pix=(0, 0), label=None): + data, dq, vp, vr, err = cube + data1, dq1, vp1, vr1, err1 = cube1 + row, col = pix + nints = data1.shape[0] + + print(" ") + print(DELIM) + if label is not None: + print("Cube Information: ({label})") + else: + print("Cube Information:") + print(f" Pixel = ({row}, {col})") + print(f" Number of Integrations = {nints}") + + print(f"data = {data[:, row, col]}") + print(f"dq = {dq[:, row, col]}") + print(f"vp = {vp[:, row, col]}") + print(f"vr = {vr[:, row, col]}") + + print(DELIM) + + +def dbg_print_slope_slope1(slopes, slopes1, pix): + data, dq, vp, vr, err = slopes + data1, dq1, vp1, vr1, err1 = slopes1 row, col = pix print(" ") @@ -468,7 +542,16 @@ def dbg_print_slope_slope1(slope, slope1, pix): print(f" Pixel = ({row}, {col})") print(f"data LIK = {data[row, col]}") - print(f"data OLS = {data1[row, col]}") + print(f"data OLS = {data1[row, col]}\n") + + # print(f"dq LIK = {dq[row, col]}") + # print(f"dq OLS = {dq1[row, col]}\n") + + print(f"vp LIK = {vp[row, col]}") + print(f"vp OLS = {vp1[row, col]}\n") + + print(f"vr LIK = {vr[row, col]}") + print(f"vr OLS = {vr1[row, col]}\n") print(DELIM) @@ -486,6 +569,15 @@ def dbg_print_cube_cube1(cube, cube1, pix): print(f" Number of Integrations = {nints}") print(f"data LIK = {data[:, row, col]}") - print(f"data OLS = {data1[:, row, col]}") + print(f"data OLS = {data1[:, row, col]}\n") + + # print(f"dq LIK = {dq[:, row, col]}") + # print(f"dq OLS = {dq1[:, row, col]}\n") + + print(f"vp LIK = {vp[:, row, col]}") + print(f"vp OLS = {vp1[:, row, col]}\n") + + print(f"vr LIK = {vr[:, row, col]}") + print(f"vr OLS = {vr1[:, row, col]}\n") print(DELIM) From 29e404edc5f0090de1ca3311c07ce905283968f0 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Tue, 11 Jun 2024 15:25:35 -0400 Subject: [PATCH 18/63] Updating likelhood algorithm initial countrate guess and differences to use, as well as modifying the creation of read patterns, when missing. --- src/stcal/ramp_fitting/likely_fit.py | 283 +++++++++++++++++++++++++-- tests/test_ramp_fitting_likly_fit.py | 13 +- 2 files changed, 279 insertions(+), 17 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index b05bd609..5f343c22 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -4,7 +4,9 @@ import multiprocessing import time import warnings + from multiprocessing import cpu_count +from pprint import pprint import numpy as np @@ -83,26 +85,37 @@ def likely_ramp_fit( "Likelihood fit requires at least 2 groups." ) - if ramp_data.read_pattern is None: - # XXX Not sure if this is the right way to do things. - # The group time maybe should be used at the end, rather than here. - readtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] - # readtimes = [(k + 1) for k in range(ngroups)] - else: - readtimes = read_data.read_pattern + readtimes = get_readtimes(ramp_data) - covar = Covar(readtimes) # XXX Choice of pedestal not given + covar = Covar(readtimes, pedestal=False) # XXX Choice of pedestal not given integ_class = IntegInfo(nints, nrows, ncols) for integ in range(nints): data = ramp_data.data[integ, :, :, :] gdq = ramp_data.groupdq[integ, :, :, :].copy() pdq = ramp_data.pixeldq[:, :].copy() + + # Eqn (5) diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] + alldiffs2use = np.ones(diff.shape, np.uint8) + for row in range(nrows): - d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) - result = fit_ramps(diff[:, row], covar, gain_2d[row], readnoise_2d[row], diffs2use=d2use) + # d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) + # XXX Fails for short ramps + d2use, countrates = mask_jumps( + diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=alldiffs2use[:, row] + ) + + # XXX detect_jump needs to be passed here. + result = fit_ramps( + diff[:, row], + covar, + gain_2d[row], + readnoise_2d[row], + diffs2use=d2use, + countrateguess=countrates * (countrates > 0), + ) integ_class.get_results(result, integ, row) pdq = utils.dq_compress_sect(ramp_data, integ, gdq, pdq) @@ -116,6 +129,251 @@ def likely_ramp_fit( return image_info, integ_info, opt_info +#BOX START end 301 +def mask_jumps( + diffs, + Cov, + rnoise, + gain, + threshold_oneomit=20.25, + threshold_twoomit=23.8, + diffs2use=None): + + """ + + Function mask_jumps implements a likelihood-based, iterative jump + detection algorithm. + + Arguments: + 1. diffs [resultant differences] + 2. Cov [class Covar, holds the covariance matrix information. Must + be based on differences alone (i.e. without the pedestal)] + 3. rnoise [read noise, 1D array] + 4. gain [gain, 1D array] + Optional arguments: + 5. threshold_oneomit [float, minimum chisq improvement to exclude + a single resultant difference. Default 20.25, + i.e., 4.5 sigma] + 6. threshold_twoomit [float, minimum chisq improvement to exclude + two sequential resultant differences. + Default 23.8, i.e., 4.5 sigma] + 7. diffs2use [a 2D array of the same shape as d, one for resultant + differences that appear ok and zero for resultant + differences flagged as contaminated. These flagged + differences will be ignored throughout jump detection, + which will only flag additional differences and + overwrite the data in this array. Default None] + + Returns: + 1. diffs2use [a 2D array of the same shape as d, one for resultant + differences that appear ok and zero for resultant + differences flagged as contaminated.] + 2. countrates [a 1D array of the count rates after masking the pixels + and resultants in diffs2use.] + + """ + # XXX refactor + if Cov.pedestal: + raise ValueError("Cannot mask jumps with a Covar class that includes a pedestal fit.") + + # Force a copy of the input array for more efficient memory access. + + d = diffs * 1 # XXX Change this name! + + # We can use one-omit searches only where the reads immediately + # preceding and following have just one read. If a readout + # pattern has more than one read per resultant but significant + # gaps between resultants then a one-omit search might still be a + # good idea even with multiple-read resultants. + + oneomit_ok = Cov.Nreads[1:] * Cov.Nreads[:-1] >= 1 + oneomit_ok[0] = oneomit_ok[-1] = True + + print("-" * 80) + print(f"{Cov.Nreads = }") + print("-" * 80) + + print("-" * 80) + print(f"{oneomit_ok = }") + print("-" * 80) + + # Other than that, we need to omit two. If a resultant has more + # than two reads, we need to omit both differences containing it + # (one pair of omissions in the differences). + + twoomit_ok = Cov.Nreads[1:-1] > 1 + + print("-" * 80) + print(f"{twoomit_ok = }") + print("-" * 80) + + # This is the array to return: one for resultant differences to + # use, zero for resultant differences to ignore. + + if diffs2use is None: + diffs2use = np.ones(d.shape, np.uint8) + + # We need to estimate the covariance matrix. I'll use the median + # here for now to limit problems with the count rate in reads with + # jumps (which is what we are looking for) since we'll be using + # likelihoods and chi squared; getting the covariance matrix + # reasonably close to correct is important. + + countrateguess = np.median(d, axis=0)[np.newaxis, :] + countrateguess *= countrateguess > 0 + + # boolean arrays to be used later + recheck = np.ones(d.shape[1]) == 1 + dropped = np.ones(d.shape[1]) == 0 + + for j in range(d.shape[0]): + + # No need for indexing on the first pass. + if j == 0: + result = fit_ramps( + d, + Cov, + gain, + rnoise, + countrateguess=countrateguess, + diffs2use=diffs2use, + detect_jumps=True) + # Also save the count rates so that we can use them later + # for debiasing. + countrate = result.countrate*1. + else: + result = fit_ramps( + d[:, recheck], + Cov, + gain[recheck], + rnoise[recheck], + countrateguess=countrateguess[:, recheck], + diffs2use=diffs2use[:, recheck], + detect_jumps=True) + + # Chi squared improvements + + dchisq_two = result.chisq - result.chisq_twoomit + dchisq_one = result.chisq - result.chisq_oneomit + + # We want the largest chi squared difference + + print(f"{dchisq_one = }") + print(f"{oneomit_ok.shape = }") + print(f"{oneomit_ok = }") + print(f"{oneomit_ok[:, np.newaxis] = }") + print(f"{dchisq_two = }") + print(f"{twoomit_ok.shape = }") + print(f"{twoomit_ok = }") + print(f"{twoomit_ok[:, np.newaxis] = }") + + import ipdb; ipdb.set_trace() + best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) + best_dchisq_two = np.amax(dchisq_two * twoomit_ok[:, np.newaxis], axis=0) # XXX HERE + + # Is the best improvement from dropping one resultant + # difference or two? Two drops will always offer more + # improvement than one so penalize them by the respective + # thresholds. Then find the chi squared improvement + # corresponding to dropping either one or two reads, whichever + # is better, if either exceeded the threshold. + + onedropbetter = (best_dchisq_one - threshold_oneomit > best_dchisq_two - threshold_twoomit) + + best_dchisq = best_dchisq_one*(best_dchisq_one > threshold_oneomit)*onedropbetter + best_dchisq += best_dchisq_two*(best_dchisq_two > threshold_twoomit)*(~onedropbetter) + + # If nothing exceeded the threshold set the improvement to + # NaN so that dchisq==best_dchisq is guaranteed to be False. + + best_dchisq[best_dchisq == 0] = np.nan + + # Now make the masks for which resultant difference(s) to + # drop, count the number of ramps affected, and drop them. + # If no ramps were affected break the loop. + + dropone = dchisq_one == best_dchisq + droptwo = dchisq_two == best_dchisq + + drop = np.any([np.sum(dropone, axis=0), + np.sum(droptwo, axis=0)], axis=0) + + if np.sum(drop) == 0: + break + + # Store the updated counts with omitted reads + + new_cts = np.zeros(np.sum(recheck)) + i_d1 = np.sum(dropone, axis=0) > 0 + new_cts[i_d1] = np.sum(result.countrate_oneomit * dropone, axis=0)[i_d1] + i_d2 = np.sum(droptwo, axis=0) > 0 + new_cts[i_d2] = np.sum(result.countrate_twoomit * droptwo, axis=0)[i_d2] + + # zero out count rates with drops and add their new values back in + + countrate[recheck] *= drop == 0 + countrate[recheck] += new_cts + + # Drop the read (set diffs2use=0) if the boolean array is True. + + diffs2use[:, recheck] *= ~dropone + diffs2use[:-1, recheck] *= ~droptwo + diffs2use[1:, recheck] *= ~droptwo + + # No need to repeat this on the entire ramp, only re-search + # ramps that had a resultant difference dropped this time. + + dropped[:] = False + dropped[recheck] = drop + recheck[:] = dropped + + # Do not try to search for bad resultants if we have already + # given up on all but one, two, or three resultant differences + # in the ramp. If there are only two left we have no way of + # choosing which one is "good". If there are three left we + # run into trouble in case we need to discard two. + + recheck[np.sum(diffs2use, axis=0) <= 3] = False + + return diffs2use, countrate +#BOX END + + +def get_readtimes(ramp_data): + """ + Get the read times needed to compute the covariance matrices. If there is + already a read_pattern in the ramp_data class, then just get it. If not, then + one needs to be constructed. If one needs to be constructed it is assumed the + groups are evenly spaced in time, as are the frames that make up the group. If + each group has only one frame and no group gap, then a list of the group times + is returned. If nframes > 0, then a list of lists of each frame time in each + group is returned with the assumption: + group_time = (nframes + groupgap) * frame_time + + Parameters + ---------- + ramp_data : RampData + Input data necessary for computing ramp fitting. + + Returns + ------- + readtimes : list + A list of frame times for each frame used in the computation of the ramp. + """ + if ramp_data.read_pattern is not None: + return ramp_data.read_pattern + + ngroups = ramp_data.data.shape[1] + # rtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] # XXX Old + tot_frames = ramp_data.nframes + ramp_data.groupgap + tot_nreads = np.arange(1, ramp_data.nframes + 1) + rtimes = [(tot_nreads + k * tot_frames) * ramp_data.frame_time for k in range(ngroups)] + + # from pprint import pprint; pprint(f"rtimes = {rtimes}") + + return rtimes + + def compute_image_info(integ_class, ramp_data): """ Compute the diffs2use mask based on DQ flags of a row. @@ -263,7 +521,7 @@ def fit_ramps( diffs, covar, gain, - rnoise, + rnoise, # Referred to as 'sig' in fitramp repo countrateguess=None, diffs2use=None, detect_jumps=False, @@ -368,6 +626,7 @@ def fit_ramps( dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig, alpha_phnoise, alpha_readnoise, beta_phnoise, beta_readnoise ) + # --- Beginning at line 250: Paper 1 section 4 # ============================================================================= @@ -936,6 +1195,8 @@ def get_ramp_result( result.uncert = np.sqrt(scale / C) result.weights = dC / C + # XXX VAR + # alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) result.var_poisson += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 40bbc142..dd6ab698 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -385,6 +385,7 @@ def test_long_ramp(): assert diff < tol +@pytest.mark.skip(reason="Not sure what expected value is.") @pytest.mark.parametrize("ngroups", [3, 2]) @pytest.mark.parametrize("nframes", [1, 2, 4, 8]) def test_short_integrations(ngroups, nframes): @@ -541,17 +542,17 @@ def dbg_print_slope_slope1(slopes, slopes1, pix): print("Slope Information:") print(f" Pixel = ({row}, {col})") - print(f"data LIK = {data[row, col]}") - print(f"data OLS = {data1[row, col]}\n") + print(f"data LIK = {data[row, col]:.12f}") + print(f"data OLS = {data1[row, col]:.12f}\n") # print(f"dq LIK = {dq[row, col]}") # print(f"dq OLS = {dq1[row, col]}\n") - print(f"vp LIK = {vp[row, col]}") - print(f"vp OLS = {vp1[row, col]}\n") + print(f"vp LIK = {vp[row, col]:.12f}") + print(f"vp OLS = {vp1[row, col]:.12f}\n") - print(f"vr LIK = {vr[row, col]}") - print(f"vr OLS = {vr1[row, col]}\n") + print(f"vr LIK = {vr[row, col]:.12f}") + print(f"vr OLS = {vr1[row, col]:.12f}\n") print(DELIM) From e6710d60b8e00bc1645b8ad6be1f9ad28a702812 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 12 Jun 2024 09:54:41 -0400 Subject: [PATCH 19/63] Updating the likelihood tests and removing unnecessary print statements. --- src/stcal/ramp_fitting/likely_fit.py | 24 -------------------- tests/test_ramp_fitting_likly_fit.py | 33 ++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 5f343c22..2a3b34a6 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -189,23 +189,11 @@ def mask_jumps( oneomit_ok = Cov.Nreads[1:] * Cov.Nreads[:-1] >= 1 oneomit_ok[0] = oneomit_ok[-1] = True - print("-" * 80) - print(f"{Cov.Nreads = }") - print("-" * 80) - - print("-" * 80) - print(f"{oneomit_ok = }") - print("-" * 80) - # Other than that, we need to omit two. If a resultant has more # than two reads, we need to omit both differences containing it # (one pair of omissions in the differences). twoomit_ok = Cov.Nreads[1:-1] > 1 - - print("-" * 80) - print(f"{twoomit_ok = }") - print("-" * 80) # This is the array to return: one for resultant differences to # use, zero for resultant differences to ignore. @@ -258,16 +246,6 @@ def mask_jumps( # We want the largest chi squared difference - print(f"{dchisq_one = }") - print(f"{oneomit_ok.shape = }") - print(f"{oneomit_ok = }") - print(f"{oneomit_ok[:, np.newaxis] = }") - print(f"{dchisq_two = }") - print(f"{twoomit_ok.shape = }") - print(f"{twoomit_ok = }") - print(f"{twoomit_ok[:, np.newaxis] = }") - - import ipdb; ipdb.set_trace() best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) best_dchisq_two = np.amax(dchisq_two * twoomit_ok[:, np.newaxis], axis=0) # XXX HERE @@ -369,8 +347,6 @@ def get_readtimes(ramp_data): tot_nreads = np.arange(1, ramp_data.nframes + 1) rtimes = [(tot_nreads + k * tot_frames) * ramp_data.frame_time for k in range(ngroups)] - # from pprint import pprint; pprint(f"rtimes = {rtimes}") - return rtimes diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index dd6ab698..1897936c 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -3,6 +3,7 @@ from stcal.ramp_fitting.ramp_fit import ramp_fit_class, ramp_fit_data from stcal.ramp_fitting.ramp_fit_class import RampData +from stcal.ramp_fitting.likely_fit import likely_ramp_fit test_dq_flags = { @@ -385,14 +386,38 @@ def test_long_ramp(): assert diff < tol -@pytest.mark.skip(reason="Not sure what expected value is.") -@pytest.mark.parametrize("ngroups", [3, 2]) +def test_2group_ramp(): + """ + It's supposed to fail. The likelihood algorithm needs at least two + groups to work. + """ + nints, ngroups, nrows, ncols = 1, 2, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 1, 0 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + with pytest.raises(ValueError): + image_info, integ_info, opt_info = likely_ramp_fit( + ramp_data, 512, save_opt, rnoise2d, gain2d, "optimal", ncores + ) + + @pytest.mark.parametrize("nframes", [1, 2, 4, 8]) -def test_short_integrations(ngroups, nframes): +def test_short_integrations(nframes): """ Check short 3 and 2 group integrations. """ - nints, nrows, ncols = 1, 1, 1 + nints, ngroups, nrows, ncols = 1, 3, 1, 1 rnval, gval = 10.0, 5.0 # frame_time, nframes, groupgap = 10.736, 4, 1 frame_time, groupgap = 10.736, 1 From 270627ec6cae6329d6b9d6e88e70f4f7f12195e3 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 13 Jun 2024 09:12:10 -0400 Subject: [PATCH 20/63] Refactoring the code. --- src/stcal/ramp_fitting/likely_fit.py | 356 +++++++++++++++------------ 1 file changed, 201 insertions(+), 155 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2a3b34a6..f4eac733 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -102,12 +102,12 @@ def likely_ramp_fit( for row in range(nrows): # d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) - # XXX Fails for short ramps + # XXX Should this be done like this? Jump detection is done here. d2use, countrates = mask_jumps( diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=alldiffs2use[:, row] ) - # XXX detect_jump needs to be passed here. + alldiffs2use[:, row] = d2use result = fit_ramps( diff[:, row], covar, @@ -129,7 +129,6 @@ def likely_ramp_fit( return image_info, integ_info, opt_info -#BOX START end 301 def mask_jumps( diffs, Cov, @@ -140,45 +139,52 @@ def mask_jumps( diffs2use=None): """ - Function mask_jumps implements a likelihood-based, iterative jump detection algorithm. - Arguments: - 1. diffs [resultant differences] - 2. Cov [class Covar, holds the covariance matrix information. Must - be based on differences alone (i.e. without the pedestal)] - 3. rnoise [read noise, 1D array] - 4. gain [gain, 1D array] - Optional arguments: - 5. threshold_oneomit [float, minimum chisq improvement to exclude - a single resultant difference. Default 20.25, - i.e., 4.5 sigma] - 6. threshold_twoomit [float, minimum chisq improvement to exclude - two sequential resultant differences. - Default 23.8, i.e., 4.5 sigma] - 7. diffs2use [a 2D array of the same shape as d, one for resultant - differences that appear ok and zero for resultant - differences flagged as contaminated. These flagged - differences will be ignored throughout jump detection, - which will only flag additional differences and - overwrite the data in this array. Default None] - - Returns: - 1. diffs2use [a 2D array of the same shape as d, one for resultant - differences that appear ok and zero for resultant - differences flagged as contaminated.] - 2. countrates [a 1D array of the count rates after masking the pixels - and resultants in diffs2use.] + Parameters + ---------- + diffs : ndarray + The group differences of the data array for a given integration and row + (ngroups-1, ncols). + + Cov : Covar + The class that computes and contains the covariance matrix info. + + rnoise : ndarray + The read noise (ncols,) + + gain : ndarray + The gain (ncols,) + + threshold_oneomit : float + Minimum chisq improvement to exclude a single resultant difference. + Default: 20.25. + + threshold_twoomit : float + Minimum chisq improvement to exclude two sequential resultant differences. + Default 23.8. + + d2use : ndarray + A boolean array definined the segmented ramps for each pixel in a row. + (ngroups-1, ncols) + + Returns + ------- + d2use : ndarray + A boolean array definined the segmented ramps for each pixel in a row. + (ngroups-1, ncols) + + countrates : ndarray + Count rate estimates used to estimate the covariance matrix. + Optional, default is None. """ - # XXX refactor if Cov.pedestal: raise ValueError("Cannot mask jumps with a Covar class that includes a pedestal fit.") # Force a copy of the input array for more efficient memory access. - - d = diffs * 1 # XXX Change this name! + loc_diff = diffs * 1 # We can use one-omit searches only where the reads immediately # preceding and following have just one read. If a readout @@ -199,7 +205,7 @@ def mask_jumps( # use, zero for resultant differences to ignore. if diffs2use is None: - diffs2use = np.ones(d.shape, np.uint8) + diffs2use = np.ones(loc_diff.shape, np.uint8) # We need to estimate the covariance matrix. I'll use the median # here for now to limit problems with the count rate in reads with @@ -207,19 +213,19 @@ def mask_jumps( # likelihoods and chi squared; getting the covariance matrix # reasonably close to correct is important. - countrateguess = np.median(d, axis=0)[np.newaxis, :] + countrateguess = np.median(loc_diff, axis=0)[np.newaxis, :] countrateguess *= countrateguess > 0 # boolean arrays to be used later - recheck = np.ones(d.shape[1]) == 1 - dropped = np.ones(d.shape[1]) == 0 + recheck = np.ones(loc_diff.shape[1]) == 1 + dropped = np.ones(loc_diff.shape[1]) == 0 - for j in range(d.shape[0]): + for j in range(loc_diff.shape[0]): # No need for indexing on the first pass. if j == 0: result = fit_ramps( - d, + loc_diff, Cov, gain, rnoise, @@ -231,7 +237,7 @@ def mask_jumps( countrate = result.countrate*1. else: result = fit_ramps( - d[:, recheck], + loc_diff[:, recheck], Cov, gain[recheck], rnoise[recheck], @@ -314,7 +320,6 @@ def mask_jumps( recheck[np.sum(diffs2use, axis=0) <= 3] = False return diffs2use, countrate -#BOX END def get_readtimes(ramp_data): @@ -605,126 +610,167 @@ def fit_ramps( # --- Beginning at line 250: Paper 1 section 4 - # ============================================================================= - # ============================================================================= - # ============================================================================= - # XXX Refactor the below section. In fact the whole thing should be moved to - # another function, which itself should be refactored. + if detect_jumps: + result = compute_jump_detects( + result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar + ) - # The code below computes the best chi squared, best-fit slope, - # and its uncertainty leaving out each group difference in - # turn. There are ndiffs possible differences that can be - # omitted. - # - # Then do it omitting two consecutive reads. There are ndiffs-1 - # possible pairs of adjacent reads that can be omitted. + return result +# RAMP FITTING END + + +def compute_jump_detects( + result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar +): + """ + The code below computes the best chi squared, best-fit slope, + and its uncertainty leaving out each group difference in + turn. There are ndiffs possible differences that can be + omitted. + + Then do it omitting two consecutive reads. There are ndiffs-1 + possible pairs of adjacent reads that can be omitted. + + This approach would need to be modified if also fitting the + pedestal, so that condition currently triggers an error. The + modifications would make the equations significantly more + complicated; the matrix equations to be solved by hand would be + larger. + + Paper II, sections 3.1 and 3.2 + + Parameters + ---------- + result : Ramp_Result + The results of the ramp fitting for a given row of pixels in an integration. + + ndiffs : int + Number of differences. + + diffs2use : ndarray + Boolean mask determining with group differences to use (ngroups-1, ncols). + + dC : ndarray + Intermediate computation. + + dB : ndarray + Intermediate computation. + + A : ndarray + Intermediate computation. + + B : ndarray + Intermediate computation. + + C : ndarray + Intermediate computation. + + scale : ndarray or integer + Overflow/underflow prevention scale. + + beta : ndarray + Off diagonal of covariance matrix. + + phi : ndarray + Intermediate computation. + + theta : ndarray + Intermediate computation. + + covar : Covar + The class that computes and contains the covariance matrix info. + + + Returns + ------- + result : Ramp_Result + The results of the ramp fitting for a given row of pixels in an integration. + """ + # The algorithms below do not work if we are computing the + # pedestal here. + if covar.pedestal: + raise ValueError( + "Cannot use jump detection algorithm when fitting pedestals." + ) + + # Diagonal elements of the inverse covariance matrix + Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] + Cinv_diag *= diffs2use + + # Off-diagonal elements of the inverse covariance matrix + # one spot above and below for the case of two adjacent + # differences to be masked + Cinv_offdiag = -beta * theta[:-2] * phi[2:] / theta[ndiffs] + + # Equations in the paper: best-fit a, b # - # This approach would need to be modified if also fitting the - # pedestal, so that condition currently triggers an error. The - # modifications would make the equations significantly more - # complicated; the matrix equations to be solved by hand would be - # larger. - - # XXX - This needs to get moved into a separate function. This section should - # be separated anyway, since it's a completely separate function, but the - # code itself should be further broken down, as it's a meandering mess - # also. Far too complicated for a single function. - # Paper II, sections 3.1 and 3.2 - if detect_jumps: + # Catch warnings in case there are masked group + # differences, since these will be overwritten later. No need + # to warn about division by zero here. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + a = (Cinv_diag * B - dB * dC) / (C * Cinv_diag - dC**2) + b = (dB - a * dC) / Cinv_diag + + result.countrate_oneomit = a + result.jumpval_oneomit = b + + # Use the best-fit a, b to get chi squared + result.chisq_oneomit = ( + A + + a**2 * C + - 2 * a * B + + b**2 * Cinv_diag + - 2 * b * dB + + 2 * a * b * dC + ) - # The algorithms below do not work if we are computing the - # pedestal here. - - if covar.pedestal: - raise ValueError( - "Cannot use jump detection algorithm when fitting pedestals." - ) - - # Diagonal elements of the inverse covariance matrix - - Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] - Cinv_diag *= diffs2use - - # Off-diagonal elements of the inverse covariance matrix - # one spot above and below for the case of two adjacent - # differences to be masked - - Cinv_offdiag = -beta * theta[:-2] * phi[2:] / theta[ndiffs] - - # Equations in the paper: best-fit a, b - # - # Catch warnings in case there are masked group - # differences, since these will be overwritten later. No need - # to warn about division by zero here. - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - a = (Cinv_diag * B - dB * dC) / (C * Cinv_diag - dC**2) - b = (dB - a * dC) / Cinv_diag - - result.countrate_oneomit = a - result.jumpval_oneomit = b - - # Use the best-fit a, b to get chi squared - - result.chisq_oneomit = ( - A - + a**2 * C - - 2 * a * B - + b**2 * Cinv_diag - - 2 * b * dB - + 2 * a * b * dC - ) - # invert the covariance matrix of a, b to get the uncertainty on a - result.uncert_oneomit = np.sqrt(Cinv_diag / (C * Cinv_diag - dC**2)) - result.jumpsig_oneomit = np.sqrt(C / (C * Cinv_diag - dC**2)) - - result.chisq_oneomit /= scale - result.uncert_oneomit *= np.sqrt(scale) - result.jumpsig_oneomit *= np.sqrt(scale) - - # Now for two omissions in a row. This is more work. Again, - # all equations are in the paper. I first define three - # factors that will be used more than once to save a bit of - # computational effort. - - cpj_fac = dC[:-1] ** 2 - C * Cinv_diag[:-1] - cjck_fac = dC[:-1] * dC[1:] - C * Cinv_offdiag - bcpj_fac = B * dC[:-1] - dB[:-1] * C - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - # best-fit a, b, c - c = bcpj_fac / cpj_fac - (B * dC[1:] - dB[1:] * C) / cjck_fac - c /= cjck_fac / cpj_fac - (dC[1:] ** 2 - C * Cinv_diag[1:]) / cjck_fac - b = (bcpj_fac - c * cjck_fac) / cpj_fac - a = (B - b * dC[:-1] - c * dC[1:]) / C - result.countrate_twoomit = a - - # best-fit chi squared - result.chisq_twoomit = ( - A + a**2 * C + b**2 * Cinv_diag[:-1] + c**2 * Cinv_diag[1:] - ) - result.chisq_twoomit -= 2 * a * B + 2 * b * dB[:-1] + 2 * c * dB[1:] - result.chisq_twoomit += ( - 2 * a * b * dC[:-1] + 2 * a * c * dC[1:] + 2 * b * c * Cinv_offdiag - ) - result.chisq_twoomit /= scale - - # uncertainty on the slope from inverting the (a, b, c) - # covariance matrix - fac = Cinv_diag[1:] * Cinv_diag[:-1] - Cinv_offdiag**2 - term2 = dC[:-1] * (dC[:-1] * Cinv_diag[1:] - Cinv_offdiag * dC[1:]) - term3 = dC[1:] * (dC[:-1] * Cinv_offdiag - Cinv_diag[:-1] * dC[1:]) - result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) - result.uncert_twoomit *= np.sqrt(scale) - - result.fill_masked_reads(diffs2use) + # invert the covariance matrix of a, b to get the uncertainty on a + result.uncert_oneomit = np.sqrt(Cinv_diag / (C * Cinv_diag - dC**2)) + result.jumpsig_oneomit = np.sqrt(C / (C * Cinv_diag - dC**2)) + + result.chisq_oneomit /= scale + result.uncert_oneomit *= np.sqrt(scale) + result.jumpsig_oneomit *= np.sqrt(scale) + + # Now for two omissions in a row. This is more work. Again, + # all equations are in the paper. I first define three + # factors that will be used more than once to save a bit of + # computational effort. + cpj_fac = dC[:-1] ** 2 - C * Cinv_diag[:-1] + cjck_fac = dC[:-1] * dC[1:] - C * Cinv_offdiag + bcpj_fac = B * dC[:-1] - dB[:-1] * C + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # best-fit a, b, c + c = bcpj_fac / cpj_fac - (B * dC[1:] - dB[1:] * C) / cjck_fac + c /= cjck_fac / cpj_fac - (dC[1:] ** 2 - C * Cinv_diag[1:]) / cjck_fac + b = (bcpj_fac - c * cjck_fac) / cpj_fac + a = (B - b * dC[:-1] - c * dC[1:]) / C + result.countrate_twoomit = a + + # best-fit chi squared + result.chisq_twoomit = ( + A + a**2 * C + b**2 * Cinv_diag[:-1] + c**2 * Cinv_diag[1:] + ) + result.chisq_twoomit -= 2 * a * B + 2 * b * dB[:-1] + 2 * c * dB[1:] + result.chisq_twoomit += ( + 2 * a * b * dC[:-1] + 2 * a * c * dC[1:] + 2 * b * c * Cinv_offdiag + ) + result.chisq_twoomit /= scale - return result + # uncertainty on the slope from inverting the (a, b, c) + # covariance matrix + fac = Cinv_diag[1:] * Cinv_diag[:-1] - Cinv_offdiag**2 + term2 = dC[:-1] * (dC[:-1] * Cinv_diag[1:] - Cinv_offdiag * dC[1:]) + term3 = dC[1:] * (dC[:-1] * Cinv_offdiag - Cinv_diag[:-1] * dC[1:]) + result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) + result.uncert_twoomit *= np.sqrt(scale) + result.fill_masked_reads(diffs2use) -# RAMP FITTING END + return result def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): From a624de98049a5779d41ab2e0b2c1c3b90b0d7f9b Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 13 Jun 2024 09:13:47 -0400 Subject: [PATCH 21/63] Updating the algorithm to choose the likelihood algorithm based on a minimum number of NGROUPS. This will need to work with the pipeline in order to make sure that if the likelihood algorithm runs the pipeline jump detect is not run and if the likelihood algorithm is not run, then the pipeline jump detect should run. --- src/stcal/ramp_fitting/ramp_fit.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index 3dbabaab..ad9803d0 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -260,12 +260,16 @@ def ramp_fit_data( Object containing optional GLS-specific ramp fitting data for the exposure """ + # For the LIKELY algorithm, due to the jump detection portion of the code + # a minimum of a four group ramp is needed. + ngroups = ramp_data.data.shape[1] + likely_min_ngroups = 4 if algorithm.upper() == "GLS": image_info, integ_info, gls_opt_info = gls_fit.gls_ramp_fit( ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, max_cores ) opt_info = None - elif algorithm.upper() == "LIKELY": + elif algorithm.upper() == "LIKELY" and ngroups >= likely_min_ngroups: image_info, integ_info, opt_info = likely_fit.likely_ramp_fit( ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores ) @@ -274,6 +278,12 @@ def ramp_fit_data( # Default to OLS. # Get readnoise array for calculation of variance of noiseless ramps, and # gain array in case optimal weighting is to be done + # XXX If the LIKELY is selected, log that the "OLS" algorithm is being use + # and note the minimum number of ngroups needed. + if algorithm.upper() == "LIKELY" and ngroups < likely_min_ngroups: + msg = f"The 'OLS' algorithm is used since the LIKELY algorithm requires {likely_min_ngroups} or more" + msg += f"NGROUPS. The NGROUPS for this data,{ngroups}, is insufficient." + log.warning(msg) nframes = ramp_data.nframes readnoise_2d *= gain_2d / np.sqrt(2.0 * nframes) From 1b2e379162d1e575f8e19b0807e4373eac103af6 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 13 Jun 2024 09:14:21 -0400 Subject: [PATCH 22/63] Updating the tests for the likelihood algorithm. --- tests/test_ramp_fitting_likly_fit.py | 61 ++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 1897936c..cd729ad4 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -160,6 +160,8 @@ def test_basic_ramp(): tol = 1.e-5 diff = abs(data - check) assert diff < tol + slope = slopes[0][0, 0] + assert abs(slope - data) < tol # Check against OLS. @@ -386,12 +388,13 @@ def test_long_ramp(): assert diff < tol -def test_2group_ramp(): +@pytest.mark.parametrize("ngroups", [1, 2]) +def test_too_few_group_ramp(ngroups): """ It's supposed to fail. The likelihood algorithm needs at least two groups to work. """ - nints, ngroups, nrows, ncols = 1, 2, 1, 1 + nints, nrows, ncols = 1, 1, 1 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 1, 0 @@ -413,11 +416,11 @@ def test_2group_ramp(): @pytest.mark.parametrize("nframes", [1, 2, 4, 8]) -def test_short_integrations(nframes): +def test_short_group_ramp(nframes): """ - Check short 3 and 2 group integrations. + Test short ramps with various nframes. """ - nints, ngroups, nrows, ncols = 1, 3, 1, 1 + nints, ngroups, nrows, ncols = 1, 4, 1, 1 rnval, gval = 10.0, 5.0 # frame_time, nframes, groupgap = 10.736, 4, 1 frame_time, groupgap = 10.736, 1 @@ -461,12 +464,8 @@ def test_short_integrations(nframes): assert diff < tol -def test_1group(): - """ - The number of groups must be greater than 1, so make sure an - exception is raised where ngroups == 1. - """ - nints, ngroups, nrows, ncols = 1, 1, 1, 1 +def data_small_good_groups(): + nints, ngroups, nrows, ncols = 1, 10, 1, 1 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 4, 1 @@ -480,12 +479,42 @@ def test_1group(): ramp = np.array(list(range(ngroups))) * 20 + 10 ramp_data.data[0, :, 0, 0] = ramp + dq = np.array([SAT] * ngroups, dtype=np.uint8) + ramp_data.groupdq[0, :, 0, 0] = dq + + return ramp_data, gain2d, rnoise2d + + + +@pytest.mark.parametrize("ngood", [1, 2]) +def test_small_good_groups(ngood): + """ + Test ramps with only one or two good groups. + """ + ramp_data, gain2d, rnoise2d = data_small_good_groups() + ramp_data.groupdq[0, :ngood, 0, 0] = GOOD + save_opt, algo, ncores = False, "LIKELY", "none" - with pytest.raises(ValueError): - slopes, cube, ols_opt, gls_opt = ramp_fit_data( - ramp_data, 512, save_opt, rnoise2d, gain2d, algo, - "optimal", ncores, test_dq_flags - ) + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + lik_slope = slopes[0][0, 0] + + + # Check against OLS. + ramp_data1, gain2d1, rnoise2d1 = data_small_good_groups() + ramp_data1.groupdq[0, :ngood, 0, 0] = GOOD + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + ols_slope = slopes1[0][0, 0] + + tol = 1.e-4 + diff = abs(ols_slope - lik_slope) + assert diff < tol # ----------------------------------------------------------------- From 886cd30b403e8ffd35e4c3d9693ee2ac24144a34 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 13 Jun 2024 13:52:07 -0400 Subject: [PATCH 23/63] Removing unneeded comments. --- src/stcal/ramp_fitting/likely_fit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index f4eac733..8dbd045d 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -101,8 +101,6 @@ def likely_ramp_fit( alldiffs2use = np.ones(diff.shape, np.uint8) for row in range(nrows): - # d2use = determine_diffs2use(ramp_data, integ, row, diff[:, row]) - # XXX Should this be done like this? Jump detection is done here. d2use, countrates = mask_jumps( diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=alldiffs2use[:, row] ) From b4df65963e32ab0786b6c2bd0024d5fe74373ad8 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 21 Jun 2024 15:36:31 -0400 Subject: [PATCH 24/63] Updating the description of ramp fitting to include the likelihood algorithm documentation. --- docs/stcal/ramp_fitting/description.rst | 50 ++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index 0fd8279c..c515f630 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -2,15 +2,14 @@ Description =========== This step determines the mean count rate, in units of counts per second, for -each pixel by performing a linear fit to the data in the input file. The fit -is done using the "ordinary least squares" method. -The fit is performed independently for each pixel. +each pixel by performing a linear fit to the data in the input file. The default +is done using the "ordinary least squares" method using based on the Fixsen fitting +algorithm described by +`Fixsen et al. (2011) `_. The count rate for each pixel is determined by a linear fit to the cosmic-ray-free and saturation-free ramp intervals for each pixel; hereafter -this interval will be referred to as a "segment." The fitting algorithm uses an -'optimal' weighting scheme, as described by -`Fixsen et al. (2011) `_. +this interval will be referred to as a "segment." Segments are determined using the 4-D GROUPDQ array of the input data set, under the assumption that the jump @@ -19,6 +18,10 @@ saturation flags are found. Pixels are processed simultaneously in blocks using the array-based functionality of numpy. The size of the block depends on the image size and the number of groups. +There is a likelihood algorithm implemented based on Timoth Brandt's papers: +Optimal Fitting and Debiasing for Detectors Read Out Up-the-Ramp +Likelihood-Based Jump Detection and Cosmic Ray Rejection for Detectors Read Out Up-the-Ramp + .. _ramp_output_products: Output Products @@ -317,3 +320,38 @@ that pixel will be flagged as JUMP_DET in the corresponding integration in the "rateints" product. That pixel will also be flagged as JUMP_DET in the "rate" product. +Likelihood Algorithm Details +---------------------------- +As an alternative to the OLS algorithm, a likelihood algorithm can be selected +with for ``--ramp_fitting.algorithm=LIKELY``. If this algorithm is selected, +the normal pipeline jump detection algorithm is skipped because this algorithm +has its own jump detection algorithm. The jump detection for this algorithm +requires NGROUPS to be a minimum of four (4). If NGROUPS :math:`\le` 3, then +this algorithm is deselected, defaulting to the above described OLS algorithm +and the normal jump detection pipeline step is run. + +Each pixel is independently processed, but rather than operate on the each +group/resultant directly, the likelihood algorithm is based on differences of +the groups/resultants :math:`d_i = r_i - r_{i-1}`. The model used to determine +the slope/countrate, :math:`a`, is: + +.. math:: + \chi^2 = ({\bf d} - a \cdot {\bf 1})^T C ({\bf d} - a \cdot {\bf 1}) \,, + +Differentiating, setting to zero, then solving for :math:`a` results in + +.. math:: + a = ({\bf 1}^T C {\bf d})({\bf 1}^T C {\bf 1})^T \,, + +The covariance matrix :math:`C` is a tridiagonal matrix, due to the nature of the +differences. Because the covariance matrix is tridiagonal, the computational +complexity from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation +and computations implemented, refer to +`Brandt (2024) `_. The Poisson and read noise +variance computations are based on equations (27) and (28), defining +:math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the off diagonal. + +This algorithm runs ramp fitting twice. The first run allows for a first +approximation for the slope, hence :math:`C`, as well as to take care for any jumps +in a ramp. Using this first approximation, ramp fitting is run again without jump +detection to compute the final slope and variances for each pixel. From f28fddf54d3b1a566a26beadcf8789ee93c87458 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 21 Jun 2024 15:38:45 -0400 Subject: [PATCH 25/63] Some minor editing. Noting where bad groups need to be identified. Updating the collapse from cube to image computations. --- src/stcal/ramp_fitting/likely_fit.py | 41 ++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 8dbd045d..fe01da6f 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -98,6 +98,9 @@ def likely_ramp_fit( # Eqn (5) diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] + # XXX apply SATURATED and DO_NOT_USE flags here + # Possibly use 'def determine_diffs2use(ramp_data, integ, row, diffs):' + # Suggested alldiffs2use[gdq[1:] != 0] alldiffs2use = np.ones(diff.shape, np.uint8) for row in range(nrows): @@ -189,19 +192,16 @@ def mask_jumps( # pattern has more than one read per resultant but significant # gaps between resultants then a one-omit search might still be a # good idea even with multiple-read resultants. - oneomit_ok = Cov.Nreads[1:] * Cov.Nreads[:-1] >= 1 oneomit_ok[0] = oneomit_ok[-1] = True # Other than that, we need to omit two. If a resultant has more # than two reads, we need to omit both differences containing it # (one pair of omissions in the differences). - twoomit_ok = Cov.Nreads[1:-1] > 1 # This is the array to return: one for resultant differences to # use, zero for resultant differences to ignore. - if diffs2use is None: diffs2use = np.ones(loc_diff.shape, np.uint8) @@ -210,7 +210,6 @@ def mask_jumps( # jumps (which is what we are looking for) since we'll be using # likelihoods and chi squared; getting the covariance matrix # reasonably close to correct is important. - countrateguess = np.median(loc_diff, axis=0)[np.newaxis, :] countrateguess *= countrateguess > 0 @@ -244,12 +243,10 @@ def mask_jumps( detect_jumps=True) # Chi squared improvements - dchisq_two = result.chisq - result.chisq_twoomit dchisq_one = result.chisq - result.chisq_oneomit # We want the largest chi squared difference - best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) best_dchisq_two = np.amax(dchisq_two * twoomit_ok[:, np.newaxis], axis=0) # XXX HERE @@ -259,7 +256,6 @@ def mask_jumps( # thresholds. Then find the chi squared improvement # corresponding to dropping either one or two reads, whichever # is better, if either exceeded the threshold. - onedropbetter = (best_dchisq_one - threshold_oneomit > best_dchisq_two - threshold_twoomit) best_dchisq = best_dchisq_one*(best_dchisq_one > threshold_oneomit)*onedropbetter @@ -267,13 +263,11 @@ def mask_jumps( # If nothing exceeded the threshold set the improvement to # NaN so that dchisq==best_dchisq is guaranteed to be False. - best_dchisq[best_dchisq == 0] = np.nan # Now make the masks for which resultant difference(s) to # drop, count the number of ramps affected, and drop them. # If no ramps were affected break the loop. - dropone = dchisq_one == best_dchisq droptwo = dchisq_two == best_dchisq @@ -284,7 +278,6 @@ def mask_jumps( break # Store the updated counts with omitted reads - new_cts = np.zeros(np.sum(recheck)) i_d1 = np.sum(dropone, axis=0) > 0 new_cts[i_d1] = np.sum(result.countrate_oneomit * dropone, axis=0)[i_d1] @@ -292,19 +285,16 @@ def mask_jumps( new_cts[i_d2] = np.sum(result.countrate_twoomit * droptwo, axis=0)[i_d2] # zero out count rates with drops and add their new values back in - countrate[recheck] *= drop == 0 countrate[recheck] += new_cts # Drop the read (set diffs2use=0) if the boolean array is True. - diffs2use[:, recheck] *= ~dropone diffs2use[:-1, recheck] *= ~droptwo diffs2use[1:, recheck] *= ~droptwo # No need to repeat this on the entire ramp, only re-search # ramps that had a resultant difference dropped this time. - dropped[:] = False dropped[recheck] = drop recheck[:] = dropped @@ -314,7 +304,6 @@ def mask_jumps( # in the ramp. If there are only two left we have no way of # choosing which one is "good". If there are three left we # run into trouble in case we need to discard two. - recheck[np.sum(diffs2use, axis=0) <= 3] = False return diffs2use, countrate @@ -380,6 +369,12 @@ def compute_image_info(integ_class, ramp_data): dq = utils.dq_compress_final(integ_class.dq, ramp_data) + # XXX Feedback from Brandt that this may not be correct. + # He provided another way to combine these computations. + # + # After testing, there doesn't appear to be a difference between + # these two compuations. Manually check. + print("**** Old Computations ****") inv_vp = 1. / integ_class.var_poisson var_p = 1. / inv_vp.sum(axis=0) @@ -389,11 +384,25 @@ def compute_image_info(integ_class, ramp_data): inv_err = 1. / integ_class.err err = 1. / inv_err.sum(axis=0) + # XXX Allegedly equivalent to the computations below. Need to check. inv_err2 = 1. / (integ_class.err**2) err2 = 1. / inv_err2.sum(axis=0) slope = integ_class.data * inv_err2 slope = slope.sum(axis=0) * err2 + ''' + print("**** New Computations ****") + inv_err2 = 1. / (integ_class.err**2) + weight = inv_err2 / inv_err2.sum(axis=0) + weight2 = weight**2 + + err2 = np.sum(integ_class.err**2 * weight2, axis=0) + + err = np.sqrt(err2) + var_p = np.sum(integ_class.var_poisson * weight2, axis=0) + var_r = np.sum(integ_class.var_rnoise * weight2, axis=0) + slope = np.sum(integ_class.data * weight, axis=0) + ''' # Compute NaNs. @@ -691,6 +700,9 @@ def compute_jump_detects( "Cannot use jump detection algorithm when fitting pedestals." ) + + # XXX need to determine where DQ flagging of JUMP_DET needs to occur. + # Diagonal elements of the inverse covariance matrix Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] Cinv_diag *= diffs2use @@ -766,6 +778,7 @@ def compute_jump_detects( result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) result.uncert_twoomit *= np.sqrt(scale) + # XXX Maybe this tells where to mask. result.fill_masked_reads(diffs2use) return result From 11694fce5b29938ec1da9f71b8fc3c7ee86a8c50 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 26 Jun 2024 15:11:37 -0400 Subject: [PATCH 26/63] Updating some basic debugging tests. --- tests/test_ramp_fitting_likly_fit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index cd729ad4..ed239595 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -223,6 +223,8 @@ def test_basic_ramp_2integ(): diff = abs(data - data1) assert diff < tol + # dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + def flagged_ramp_data(): nints, ngroups, nrows, ncols = 1, 20, 1, 1 From 1e274310fac08b530d08212ec36f41cf553789e9 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 26 Jun 2024 15:12:30 -0400 Subject: [PATCH 27/63] Suppressing warnings. --- src/stcal/ramp_fitting/likely_fit.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index fe01da6f..b82f1076 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -374,7 +374,10 @@ def compute_image_info(integ_class, ramp_data): # # After testing, there doesn't appear to be a difference between # these two compuations. Manually check. - print("**** Old Computations ****") + ''' + # print("**** Old Computations ****") + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) inv_vp = 1. / integ_class.var_poisson var_p = 1. / inv_vp.sum(axis=0) @@ -390,8 +393,9 @@ def compute_image_info(integ_class, ramp_data): slope = integ_class.data * inv_err2 slope = slope.sum(axis=0) * err2 + warnings.resetwarnings() ''' - print("**** New Computations ****") + # print("**** New Computations ****") inv_err2 = 1. / (integ_class.err**2) weight = inv_err2 / inv_err2.sum(axis=0) weight2 = weight**2 @@ -402,7 +406,6 @@ def compute_image_info(integ_class, ramp_data): var_p = np.sum(integ_class.var_poisson * weight2, axis=0) var_r = np.sum(integ_class.var_rnoise * weight2, axis=0) slope = np.sum(integ_class.data * weight, axis=0) - ''' # Compute NaNs. @@ -825,6 +828,9 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): Overflow/underflow prevention scale. """ + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] alpha_readnoise = rnoise**2 * covar.alpha_readnoise[:, np.newaxis] alpha = alpha_phnoise + alpha_readnoise @@ -833,6 +839,8 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): beta_readnoise = rnoise**2 * covar.beta_readnoise[:, np.newaxis] beta = beta_phnoise + beta_readnoise + warnings.resetwarnings() + ndiffs, npix = diffs.shape # Rescale the covariance matrix to a determinant of 1 to @@ -852,6 +860,9 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): theta[1] = alpha[0] scale = theta[0] * 1 + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + for i in range(2, ndiffs + 1): theta[i] = alpha[i-1] / scale * theta[i-1] \ - beta[i-2]**2 / scale**2 * theta[i-2] @@ -867,6 +878,8 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): theta[i-1] /= tmp theta[i-2] /= (tmp / f) theta[i] = 1 + + warnings.resetwarnings() else: scale = 1 @@ -1225,6 +1238,9 @@ def get_ramp_result( # result.countrate = B / C result.countrate = B * invC result.chisq = (A - B**2 / C) / scale + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + result.uncert = np.sqrt(scale / C) result.weights = dC / C @@ -1238,6 +1254,8 @@ def get_ramp_result( result.var_rnoise += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0) + warnings.resetwarnings() + # If we are computing the pedestal, then we use the other formulas # in the paper. From 3ef57101abce2f0cf8a7d11ea47e47a43fdc9c90 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 28 Jun 2024 10:31:33 -0400 Subject: [PATCH 28/63] Updating comments. --- src/stcal/ramp_fitting/likely_algo_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index d9f67664..c9ce644d 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -44,7 +44,7 @@ def get_results(self, result, integ, row): Parameters ---------- result : Ramp_Result - Holds computed ramp fitting information. XXX - rename + Holds computed ramp fitting information. integ : int The current integration being operated on. @@ -305,7 +305,7 @@ def calc_bias(self, countrates, sig, cvec, da=1e-7): Calculate the bias in the best-fit count rate from estimating the covariance matrix. This calculation is derived in the paper. - Section 5 of paper 1. XXX Not sure when to use this method. + Section 5 of paper 1. Arguments: Parameters From f54eef7e2c4c7cc337cbe1327a73cf116964a1a9 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 28 Jun 2024 10:32:26 -0400 Subject: [PATCH 29/63] Updating the computation of which diffs to use during the likelihood ramp fitting. --- src/stcal/ramp_fitting/likely_fit.py | 36 +++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index b82f1076..846a236b 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -97,25 +97,30 @@ def likely_ramp_fit( # Eqn (5) diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] - - # XXX apply SATURATED and DO_NOT_USE flags here - # Possibly use 'def determine_diffs2use(ramp_data, integ, row, diffs):' - # Suggested alldiffs2use[gdq[1:] != 0] - alldiffs2use = np.ones(diff.shape, np.uint8) + alldiffs2use = np.ones(diff.shape, np.uint8) # XXX May not be necessary for row in range(nrows): + d2use = determine_diffs2use(ramp_data, integ, row, diff) d2use, countrates = mask_jumps( - diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=alldiffs2use[:, row] + diff[:, row], + covar, + readnoise_2d[row], + gain_2d[row], + diffs2use=d2use ) - alldiffs2use[:, row] = d2use + alldiffs2use[:, row] = d2use # XXX May not be necessary + + # XXX According to Brandt feedback + # rateguess = countrates * (countrates > 0) * darkrate (ramp_data.average_dark_current?) + rateguess = countrates * (countrates > 0) result = fit_ramps( diff[:, row], covar, gain_2d[row], readnoise_2d[row], diffs2use=d2use, - countrateguess=countrates * (countrates > 0), + countrateguess=rateguess ) integ_class.get_results(result, integ, row) @@ -211,6 +216,8 @@ def mask_jumps( # likelihoods and chi squared; getting the covariance matrix # reasonably close to correct is important. countrateguess = np.median(loc_diff, axis=0)[np.newaxis, :] + + # XXX Somehow add the Poisson variance back in. countrateguess *= countrateguess > 0 # boolean arrays to be used later @@ -371,9 +378,6 @@ def compute_image_info(integ_class, ramp_data): # XXX Feedback from Brandt that this may not be correct. # He provided another way to combine these computations. - # - # After testing, there doesn't appear to be a difference between - # these two compuations. Manually check. ''' # print("**** Old Computations ****") warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) @@ -387,7 +391,6 @@ def compute_image_info(integ_class, ramp_data): inv_err = 1. / integ_class.err err = 1. / inv_err.sum(axis=0) - # XXX Allegedly equivalent to the computations below. Need to check. inv_err2 = 1. / (integ_class.err**2) err2 = 1. / inv_err2.sum(axis=0) @@ -407,7 +410,7 @@ def compute_image_info(integ_class, ramp_data): var_r = np.sum(integ_class.var_rnoise * weight2, axis=0) slope = np.sum(integ_class.data * weight, axis=0) - # Compute NaNs. + # XXX Compute NaNs. return (slope, dq, var_p, var_r, err) @@ -440,7 +443,8 @@ def determine_diffs2use(ramp_data, integ, row, diffs): _, ngroups, _, ncols = ramp_data.data.shape dq = np.zeros(shape=(ngroups, ncols), dtype=np.uint8) dq[:, :] = ramp_data.groupdq[integ, :, row, :] - d2use = np.ones(shape=diffs.shape, dtype=np.uint8) + d2use_tmp = np.ones(shape=diffs.shape, dtype=np.uint8) + d2use = d2use_tmp[:, row] # The JUMP_DET is handled different than other group DQ flags. jmp = np.uint8(ramp_data.flags_jump_det) @@ -1234,12 +1238,12 @@ def get_ramp_result( # XXX pedestal is always False. if not covar.pedestal: + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) invC = 1 / C # result.countrate = B / C result.countrate = B * invC result.chisq = (A - B**2 / C) / scale - warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) - warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) result.uncert = np.sqrt(scale / C) result.weights = dC / C From 97f49e377cad1afffa3c9ffd412f2e18c2761e2f Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Sat, 29 Jun 2024 11:51:59 -0400 Subject: [PATCH 30/63] Debugging likelihood ramp fitting tests. --- tests/test_ramp_fitting_likly_fit.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index ed239595..461b2f67 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -179,6 +179,9 @@ def test_basic_ramp(): diff = abs(data - data1) assert diff < tol + print("Here") + dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + def test_basic_ramp_2integ(): """ @@ -223,7 +226,7 @@ def test_basic_ramp_2integ(): diff = abs(data - data1) assert diff < tol - # dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + dbg_print_slope_slope1(slopes, slopes1, (0, 0)) def flagged_ramp_data(): @@ -271,7 +274,7 @@ def test_flagged_ramp(): ramp_data, gain2d, rnoise2d = flagged_ramp_data() save_opt, algo, ncores = False, "OLS", "none" - slopes, cube1, ols_opt, gls_opt = ramp_fit_data( + slopes1, cube1, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) @@ -283,6 +286,8 @@ def test_flagged_ramp(): assert diff < tol assert dq == dq_ols + dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + def random_ramp_data(): nints, ngroups, nrows, ncols = 1, 10, 1, 1 @@ -388,6 +393,7 @@ def test_long_ramp(): data1 = cube1[0][0, 0, 0] diff = abs(data - data1) assert diff < tol + dbg_print_slope_slope1(slopes, slopes1, (0, 0)) @pytest.mark.parametrize("ngroups", [1, 2]) @@ -464,6 +470,7 @@ def test_short_group_ramp(nframes): data1 = cube1[0][0, 0, 0] diff = abs(data - data1) assert diff < tol + dbg_print_slope_slope1(slopes, slopes1, (0, 0)) def data_small_good_groups(): @@ -487,8 +494,9 @@ def data_small_good_groups(): return ramp_data, gain2d, rnoise2d - -@pytest.mark.parametrize("ngood", [1, 2]) +# XXX One good group in a ramp may not be any good +# @pytest.mark.parametrize("ngood", [1, 2]) +@pytest.mark.parametrize("ngood", [2]) def test_small_good_groups(ngood): """ Test ramps with only one or two good groups. @@ -516,7 +524,12 @@ def test_small_good_groups(ngood): tol = 1.e-4 diff = abs(ols_slope - lik_slope) - assert diff < tol + if ngood==2: + assert diff < tol + else: + print(f"ols_slope = {ols_slope}") + print(f"lik_slope = {lik_slope}") + print(f"diff = {diff}") # ----------------------------------------------------------------- From 92fe6b52de48814efaf29f68e3e71cb8c470d82b Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Mon, 8 Jul 2024 15:35:08 -0400 Subject: [PATCH 31/63] Updating the formatting, as well as identifying possible jump detection flagging locations. --- src/stcal/ramp_fitting/likely_algo_classes.py | 1 + src/stcal/ramp_fitting/likely_fit.py | 221 +++++++++++------- 2 files changed, 136 insertions(+), 86 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index c9ce644d..07de82cc 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -131,6 +131,7 @@ def fill_masked_reads(self, diffs2use): """ # replace entries that would be nan (from trying to # doubly exclude read differences) with the global fits. + # XXX Maybe here flag JUMP_DET omit = diffs2use == 0 ones = np.ones(diffs2use.shape) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 846a236b..3dcf539d 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -14,7 +14,7 @@ from .likely_algo_classes import IntegInfo, Ramp_Result, Covar -DELIM = '=' * 80 +DELIM = "=" * 80 log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -81,9 +81,7 @@ def likely_ramp_fit( nints, ngroups, nrows, ncols = ramp_data.data.shape if ngroups < 2: - raise ValueError( - "Likelihood fit requires at least 2 groups." - ) + raise ValueError("Likelihood fit requires at least 2 groups.") readtimes = get_readtimes(ramp_data) @@ -102,26 +100,22 @@ def likely_ramp_fit( for row in range(nrows): d2use = determine_diffs2use(ramp_data, integ, row, diff) d2use, countrates = mask_jumps( - diff[:, row], - covar, - readnoise_2d[row], - gain_2d[row], - diffs2use=d2use - ) + diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=d2use + ) alldiffs2use[:, row] = d2use # XXX May not be necessary # XXX According to Brandt feedback # rateguess = countrates * (countrates > 0) * darkrate (ramp_data.average_dark_current?) - rateguess = countrates * (countrates > 0) + rateguess = countrates * (countrates > 0) + ramp_data.average_dark_current result = fit_ramps( - diff[:, row], - covar, - gain_2d[row], - readnoise_2d[row], - diffs2use=d2use, - countrateguess=rateguess - ) + diff[:, row], + covar, + gain_2d[row], + readnoise_2d[row], + diffs2use=d2use, + countrateguess=rateguess, + ) integ_class.get_results(result, integ, row) pdq = utils.dq_compress_sect(ramp_data, integ, gdq, pdq) @@ -136,13 +130,14 @@ def likely_ramp_fit( def mask_jumps( - diffs, - Cov, - rnoise, - gain, - threshold_oneomit=20.25, - threshold_twoomit=23.8, - diffs2use=None): + diffs, + Cov, + rnoise, + gain, + threshold_oneomit=20.25, + threshold_twoomit=23.8, + diffs2use=None, +): """ Function mask_jumps implements a likelihood-based, iterative jump @@ -168,7 +163,7 @@ def mask_jumps( Default: 20.25. threshold_twoomit : float - Minimum chisq improvement to exclude two sequential resultant differences. + Minimum chisq improvement to exclude two sequential resultant differences. Default 23.8. d2use : ndarray @@ -187,11 +182,13 @@ def mask_jumps( """ if Cov.pedestal: - raise ValueError("Cannot mask jumps with a Covar class that includes a pedestal fit.") - + raise ValueError( + "Cannot mask jumps with a Covar class that includes a pedestal fit." + ) + # Force a copy of the input array for more efficient memory access. loc_diff = diffs * 1 - + # We can use one-omit searches only where the reads immediately # preceding and following have just one read. If a readout # pattern has more than one read per resultant but significant @@ -204,7 +201,7 @@ def mask_jumps( # than two reads, we need to omit both differences containing it # (one pair of omissions in the differences). twoomit_ok = Cov.Nreads[1:-1] > 1 - + # This is the array to return: one for resultant differences to # use, zero for resultant differences to ignore. if diffs2use is None: @@ -223,31 +220,33 @@ def mask_jumps( # boolean arrays to be used later recheck = np.ones(loc_diff.shape[1]) == 1 dropped = np.ones(loc_diff.shape[1]) == 0 - + for j in range(loc_diff.shape[0]): # No need for indexing on the first pass. if j == 0: result = fit_ramps( - loc_diff, - Cov, - gain, - rnoise, - countrateguess=countrateguess, - diffs2use=diffs2use, - detect_jumps=True) + loc_diff, + Cov, + gain, + rnoise, + countrateguess=countrateguess, + diffs2use=diffs2use, + detect_jumps=True, + ) # Also save the count rates so that we can use them later # for debiasing. - countrate = result.countrate*1. + countrate = result.countrate * 1.0 else: result = fit_ramps( - loc_diff[:, recheck], - Cov, - gain[recheck], - rnoise[recheck], - countrateguess=countrateguess[:, recheck], - diffs2use=diffs2use[:, recheck], - detect_jumps=True) + loc_diff[:, recheck], + Cov, + gain[recheck], + rnoise[recheck], + countrateguess=countrateguess[:, recheck], + diffs2use=diffs2use[:, recheck], + detect_jumps=True, + ) # Chi squared improvements dchisq_two = result.chisq - result.chisq_twoomit @@ -255,18 +254,26 @@ def mask_jumps( # We want the largest chi squared difference best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) - best_dchisq_two = np.amax(dchisq_two * twoomit_ok[:, np.newaxis], axis=0) # XXX HERE - + best_dchisq_two = np.amax( + dchisq_two * twoomit_ok[:, np.newaxis], axis=0 + ) # XXX HERE Is this where JUMP_DET is set? + # Is the best improvement from dropping one resultant # difference or two? Two drops will always offer more # improvement than one so penalize them by the respective # thresholds. Then find the chi squared improvement # corresponding to dropping either one or two reads, whichever # is better, if either exceeded the threshold. - onedropbetter = (best_dchisq_one - threshold_oneomit > best_dchisq_two - threshold_twoomit) - - best_dchisq = best_dchisq_one*(best_dchisq_one > threshold_oneomit)*onedropbetter - best_dchisq += best_dchisq_two*(best_dchisq_two > threshold_twoomit)*(~onedropbetter) + onedropbetter = ( + best_dchisq_one - threshold_oneomit > best_dchisq_two - threshold_twoomit + ) + + best_dchisq = ( + best_dchisq_one * (best_dchisq_one > threshold_oneomit) * onedropbetter + ) + best_dchisq += ( + best_dchisq_two * (best_dchisq_two > threshold_twoomit) * (~onedropbetter) + ) # If nothing exceeded the threshold set the improvement to # NaN so that dchisq==best_dchisq is guaranteed to be False. @@ -278,9 +285,8 @@ def mask_jumps( dropone = dchisq_one == best_dchisq droptwo = dchisq_two == best_dchisq - drop = np.any([np.sum(dropone, axis=0), - np.sum(droptwo, axis=0)], axis=0) - + drop = np.any([np.sum(dropone, axis=0), np.sum(droptwo, axis=0)], axis=0) + if np.sum(drop) == 0: break @@ -294,7 +300,7 @@ def mask_jumps( # zero out count rates with drops and add their new values back in countrate[recheck] *= drop == 0 countrate[recheck] += new_cts - + # Drop the read (set diffs2use=0) if the boolean array is True. diffs2use[:, recheck] *= ~dropone diffs2use[:-1, recheck] *= ~droptwo @@ -344,7 +350,9 @@ def get_readtimes(ramp_data): # rtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] # XXX Old tot_frames = ramp_data.nframes + ramp_data.groupgap tot_nreads = np.arange(1, ramp_data.nframes + 1) - rtimes = [(tot_nreads + k * tot_frames) * ramp_data.frame_time for k in range(ngroups)] + rtimes = [ + (tot_nreads + k * tot_frames) * ramp_data.frame_time for k in range(ngroups) + ] return rtimes @@ -364,7 +372,7 @@ def compute_image_info(integ_class, ramp_data): Returns ------- image_info : tuple - The list of arrays for the rate product. + The list of arrays for the rate product. """ if integ_class.data.shape[0] == 1: data = integ_class.data[0, :, :] @@ -378,7 +386,7 @@ def compute_image_info(integ_class, ramp_data): # XXX Feedback from Brandt that this may not be correct. # He provided another way to combine these computations. - ''' + """ # print("**** Old Computations ****") warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) @@ -397,9 +405,9 @@ def compute_image_info(integ_class, ramp_data): slope = integ_class.data * inv_err2 slope = slope.sum(axis=0) * err2 warnings.resetwarnings() - ''' + """ # print("**** New Computations ****") - inv_err2 = 1. / (integ_class.err**2) + inv_err2 = 1.0 / (integ_class.err**2) weight = inv_err2 / inv_err2.sum(axis=0) weight2 = weight**2 @@ -466,12 +474,12 @@ def determine_diffs2use(ramp_data, integ, row, diffs): # If a jump occurs at group k, then the difference # group[k] - group[k-1] is excluded. - d2use[jmp_locs[1:, :]==1] = 0 + d2use[jmp_locs[1:, :] == 1] = 0 # If a non-jump flag occurs at group k, then the differences # group[k+1] - group[k] and group[k] - group[k-1] are excluded. - d2use[oflags_locs[1:, :]==1] = 0 - d2use[oflags_locs[:-1, :]==1] = 0 + d2use[oflags_locs[1:, :] == 1] = 0 + d2use[oflags_locs[:-1, :] == 1] = 0 return d2use @@ -523,7 +531,7 @@ def fit_ramps( resetval=0, resetsig=np.inf, rescale=True, - dn_scale=10., + dn_scale=10.0, ): """ Function fit_ramps on a row of pixels. Fits ramps to read differences @@ -587,9 +595,10 @@ def fit_ramps( countrateguess = inital_countrateguess(covar, diffs, diffs2use) alpha_tuple, beta_tuple, scale = compute_abs( - countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale) - alpha, alpha_phnoise, alpha_readnoise = alpha_tuple - beta, beta_phnoise, beta_readnoise = beta_tuple + countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale + ) + alpha, alpha_phnoise, alpha_readnoise = alpha_tuple + beta, beta_phnoise, beta_readnoise = beta_tuple ndiffs, npix = diffs.shape @@ -613,13 +622,36 @@ def fit_ramps( ThetaD = compute_ThetaDs(ndiffs, npix, beta, theta, sgn, diff_mask) # EQN 48 dB, dC, A, B, C = matrix_computations( - ndiffs, npix, sgn, diff_mask, diffs2use, beta, - phi, Phi, PhiD, theta, Theta, ThetaD, + ndiffs, + npix, + sgn, + diff_mask, + diffs2use, + beta, + phi, + Phi, + PhiD, + theta, + Theta, + ThetaD, ) result = get_ramp_result( - dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig, - alpha_phnoise, alpha_readnoise, beta_phnoise, beta_readnoise + dC, + dB, + A, + B, + C, + scale, + phi, + theta, + covar, + resetval, + resetsig, + alpha_phnoise, + alpha_readnoise, + beta_phnoise, + beta_readnoise, ) # --- Beginning at line 250: Paper 1 section 4 @@ -630,6 +662,8 @@ def fit_ramps( ) return result + + # RAMP FITTING END @@ -703,10 +737,7 @@ def compute_jump_detects( # The algorithms below do not work if we are computing the # pedestal here. if covar.pedestal: - raise ValueError( - "Cannot use jump detection algorithm when fitting pedestals." - ) - + raise ValueError("Cannot use jump detection algorithm when fitting pedestals.") # XXX need to determine where DQ flagging of JUMP_DET needs to occur. @@ -841,7 +872,7 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): beta_phnoise = countrateguess / gain * covar.beta_phnoise[:, np.newaxis] beta_readnoise = rnoise**2 * covar.beta_readnoise[:, np.newaxis] - beta = beta_phnoise + beta_readnoise + beta = beta_phnoise + beta_readnoise warnings.resetwarnings() @@ -868,19 +899,21 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) for i in range(2, ndiffs + 1): - theta[i] = alpha[i-1] / scale * theta[i-1] \ - - beta[i-2]**2 / scale**2 * theta[i-2] + theta[i] = ( + alpha[i - 1] / scale * theta[i - 1] + - beta[i - 2] ** 2 / scale**2 * theta[i - 2] + ) # Scaling every ten steps in safe for alpha up to 1e20 # or so and incurs a negligible computational cost for # the fractional power. if i % int(dn_scale) == 0 or i == ndiffs: - f = theta[i]**(1/i) + f = theta[i] ** (1 / i) scale *= f tmp = theta[i] / f - theta[i-1] /= tmp - theta[i-2] /= (tmp / f) + theta[i - 1] /= tmp + theta[i - 2] /= tmp / f theta[i] = 1 warnings.resetwarnings() @@ -1175,8 +1208,22 @@ def matrix_computations( def get_ramp_result( - dC, dB, A, B, C, scale, phi, theta, covar, resetval, resetsig, - alpha_phnoise, alpha_readnoise, beta_phnoise, beta_readnoise): + dC, + dB, + A, + B, + C, + scale, + phi, + theta, + covar, + resetval, + resetsig, + alpha_phnoise, + alpha_readnoise, + beta_phnoise, + beta_readnoise, +): """ Use intermediate computations to fit the ramp and save the results. @@ -1252,11 +1299,13 @@ def get_ramp_result( # alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) result.var_poisson += 2 * np.sum( - result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0) + result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0 + ) result.var_rnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) result.var_rnoise += 2 * np.sum( - result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0) + result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0 + ) warnings.resetwarnings() From 9f530a04382c7a7c18235129edac977cf9fac118 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 09:18:41 -0400 Subject: [PATCH 32/63] Fixed shape bug when using dark current and changed the name of a function to make it more verbose. --- src/stcal/ramp_fitting/likely_fit.py | 32 ++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 3dcf539d..53156cdf 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -3,6 +3,7 @@ import logging import multiprocessing import time +import sys import warnings from multiprocessing import cpu_count @@ -14,6 +15,15 @@ from .likely_algo_classes import IntegInfo, Ramp_Result, Covar +################## DEBUG ################## +# HELP!! +import sys +sys.path.insert(1, "/Users/kmacdonald/code/common") +from general_funcs import dbg_print, \ + array_string +################## DEBUG ################## + + DELIM = "=" * 80 log = logging.getLogger(__name__) @@ -99,15 +109,27 @@ def likely_ramp_fit( for row in range(nrows): d2use = determine_diffs2use(ramp_data, integ, row, diff) + d2use_copy = d2use.copy() # Use to flag jumps d2use, countrates = mask_jumps( diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=d2use ) + + ''' + # XXX SET JUMP_DET + # Set jump detection flags + jump_locs = d2use_copy ^ d2use + jump_locs[jump_locs > 0] = ramp_data.flags_jump_det + print(f"Row: {row} {jump_locs.shape = }") + # XXX Need to figure out how to put flags in gdq + # gdq |= jump_locs + ''' + alldiffs2use[:, row] = d2use # XXX May not be necessary # XXX According to Brandt feedback # rateguess = countrates * (countrates > 0) * darkrate (ramp_data.average_dark_current?) - rateguess = countrates * (countrates > 0) + ramp_data.average_dark_current + rateguess = countrates * (countrates > 0) + ramp_data.average_dark_current[row, :] result = fit_ramps( diff[:, row], covar, @@ -222,7 +244,6 @@ def mask_jumps( dropped = np.ones(loc_diff.shape[1]) == 0 for j in range(loc_diff.shape[0]): - # No need for indexing on the first pass. if j == 0: result = fit_ramps( @@ -594,7 +615,8 @@ def fit_ramps( if countrateguess is None: countrateguess = inital_countrateguess(covar, diffs, diffs2use) - alpha_tuple, beta_tuple, scale = compute_abs( + # XXX Maybe use a better name for this function, like compute_alphas_betas + alpha_tuple, beta_tuple, scale = compute_alphas_betas( countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale ) alpha, alpha_phnoise, alpha_readnoise = alpha_tuple @@ -822,7 +844,7 @@ def compute_jump_detects( return result -def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): +def compute_alphas_betas(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): """ Compute alpha, beta, and scale needed for ramp fit. Elements of the covariance matrix. @@ -866,6 +888,8 @@ def compute_abs(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + # import ipdb; ipdb.set_trace() + alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] alpha_readnoise = rnoise**2 * covar.alpha_readnoise[:, np.newaxis] alpha = alpha_phnoise + alpha_readnoise From d8a041d013724c21d1c0472bd3dd88ad03c0f170 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 10:30:55 -0400 Subject: [PATCH 33/63] Adding jump detection flagging to the likelihood algorithm for ramp fitting. --- src/stcal/ramp_fitting/likely_fit.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 53156cdf..cb00708d 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -115,15 +115,11 @@ def likely_ramp_fit( ) - ''' # XXX SET JUMP_DET # Set jump detection flags jump_locs = d2use_copy ^ d2use jump_locs[jump_locs > 0] = ramp_data.flags_jump_det - print(f"Row: {row} {jump_locs.shape = }") - # XXX Need to figure out how to put flags in gdq - # gdq |= jump_locs - ''' + gdq[1:, row, :] |= jump_locs alldiffs2use[:, row] = d2use # XXX May not be necessary From 392a0f65d8abd27c02aff6268e5f9de6fee0c8c8 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 10:32:05 -0400 Subject: [PATCH 34/63] Adding test for proper flagging for the likelihood jump detection in ramp fitting. --- tests/test_ramp_fitting_likly_fit.py | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 461b2f67..cd1ebd1f 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -183,6 +183,37 @@ def test_basic_ramp(): dbg_print_slope_slope1(slopes, slopes1, (0, 0)) +@pytest.mark.skip(reason="Incompatible ndarray shapes.") +def test_basic_ramp_multi_pixel(): + """ + Test a basic ramp with a linear progression up the ramp. Compare the + integration results from the LIKELY algorithm to the OLS algorithm. + """ + nints, ngroups, nrows, ncols = 1, 10, 2, 2 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + ramp_data.data[0, :, 0, 1] = ramp + ramp_data.data[0, :, 1, 0] = ramp + ramp_data.data[0, :, 1, 1] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + + + def test_basic_ramp_2integ(): """ Test a basic ramp with a linear progression up the ramp. Compare the @@ -532,6 +563,46 @@ def test_small_good_groups(ngood): print(f"diff = {diff}") +def test_jump_detect(): + nints, ngroups, nrows, ncols = 1, 10, 2, 2 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 5, 2 + # frame_time, nframes, groupgap = 1., 1, 0 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a ramp with a jump to see if it gets detected. + base, cr, jump_loc = 15., 1000., 6 + ramp = np.array([(k+1) * base for k in range(ngroups)]) + ramp_data.data[0, :, 0, 1] = ramp + if nrows > 1: + ramp_data.data[0, :, 1, 0] = ramp + ramp[jump_loc:] += cr + ramp_data.data[0, :, 0, 0] = ramp + ramp[jump_loc-1] += cr + if nrows > 1: + ramp_data.data[0, :, 1, 1] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + data, dq, vp, vr, err = slopes + slope_est = base / ramp_data.group_time + + tol = 1.e-4 + assert abs(data[0, 0] - slope_est) < tol + assert dq[0, 0] == JMP + assert dq[0, 1] == GOOD + assert dq[1, 0] == GOOD + assert dq[1, 1] == JMP + + # ----------------------------------------------------------------- # DEBUG # ----------------------------------------------------------------- @@ -651,3 +722,7 @@ def dbg_print_cube_cube1(cube, cube1, pix): print(f"vr OLS = {vr1[:, row, col]}\n") print(DELIM) + + +def array_string(arr, prec=4): + return np.array2string(arr, precision=prec, max_line_width=np.nan, separator=", ") From ca4b7b28a0e7f70359e411a9cc9e20303a7ed441 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 10:37:53 -0400 Subject: [PATCH 35/63] Removing skip for multi-pixel test. --- tests/test_ramp_fitting_likly_fit.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index cd1ebd1f..ad87b61c 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -179,11 +179,7 @@ def test_basic_ramp(): diff = abs(data - data1) assert diff < tol - print("Here") - dbg_print_slope_slope1(slopes, slopes1, (0, 0)) - -@pytest.mark.skip(reason="Incompatible ndarray shapes.") def test_basic_ramp_multi_pixel(): """ Test a basic ramp with a linear progression up the ramp. Compare the @@ -211,7 +207,25 @@ def test_basic_ramp_multi_pixel(): ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) + ramp_data1, gain2d1, rnoise2d1 = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data1.data[0, :, 0, 0] = ramp + ramp_data1.data[0, :, 0, 1] = ramp + ramp_data1.data[0, :, 1, 0] = ramp + ramp_data1.data[0, :, 1, 1] = ramp + + save_opt, algo, ncores = False, "OLS", "none" + slopes1, cube1, ols_opt1, gls_opt1 = ramp_fit_data( + ramp_data1, 512, save_opt, rnoise2d1, gain2d1, algo, "optimal", ncores, test_dq_flags + ) + + data, dq, vp, vr, err = slopes + data1, dq1, vp1, vr1, err1 = slopes1 + tol = 1.e-4 + np.testing.assert_allclose(data, data1, tol) def test_basic_ramp_2integ(): From 18e58c61f5e0f944a1d7d7ad0ca22e3655819243 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 11:02:30 -0400 Subject: [PATCH 36/63] Updating the jump detection test for the ramp fitting using the likelihood algorithm. --- tests/test_ramp_fitting_likly_fit.py | 57 +++++++++++----------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index ad87b61c..b0f35336 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -334,11 +334,14 @@ def test_flagged_ramp(): dbg_print_slope_slope1(slopes, slopes1, (0, 0)) -def random_ramp_data(): +def test_random_ramp(): + """ + Created a slope with a base slope of 150., with random Poisson + noise with lambda 5.0. At group 4 is a jump of 1100.0. + """ nints, ngroups, nrows, ncols = 1, 10, 1, 1 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 5, 2 - # frame_time, nframes, groupgap = 1., 1, 0 dims = nints, ngroups, nrows, ncols var = rnval, gval @@ -352,51 +355,28 @@ def random_ramp_data(): ramp = np.array([153., 307., 457., 604., 1853., 2002., 2159., 2308., 2459., 2601.]) ramp_data.data[0, :, 0, 0] = ramp - # Create a jump. + # Create a jump, but don't mark it to make sure it gets detected. dq = np.array([GOOD] * ngroups) - dq[4] = JMP ramp_data.groupdq[0, :, 0, 0] = dq - return ramp_data, gain2d, rnoise2d - - -@pytest.mark.skip(reason="Not sure what expected value is.") -def test_random_ramp(): - """ - Created a slope with a base slope of 150., with random Poisson - noise with lambda 5.0. At group 4 is a jump of 1100.0. - Compare the integration results from the LIKELY algorithm - to the OLS algorithm. - """ - ramp_data, gain2d, rnoise2d = random_ramp_data() - dbg_print_basic_ramp(ramp_data) - save_opt, algo, ncores = False, "LIKELY", "none" slopes, cube, ols_opt, gls_opt = ramp_fit_data( ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags ) - data = cube[0][0, 0, 0] - dq = cube[1][0, 0, 0] - err = cube[-1][0, 0, 0] - - # Check against OLS. - ramp_data, gain2d, rnoise2d = random_ramp_data() - - save_opt, algo, ncores = False, "OLS", "none" - slopes1, cube1, ols_opt, gls_opt = ramp_fit_data( - ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags - ) - - data_ols = cube1[0][0, 0, 0] - dq_ols = cube1[1][0, 0, 0] - err_ols = cube1[-1][0, 0, 0] + data, dq, vp, vr, err = slopes + tol = 1.e-4 - ddiff = abs(data - data_ols) - # XXX Finish + assert abs(data[0, 0] - 1.9972216) < tol + assert dq[0, 0] == JMP + assert abs(vp[0, 0] - 0.00064461) < tol + assert abs(vr[0, 0] - 0.00018037) < tol def test_long_ramp(): + """ + Test a long ramp with hundreds of groups. + """ nints, ngroups, nrows, ncols = 1, 200, 1, 1 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 4, 1 @@ -578,6 +558,10 @@ def test_small_good_groups(ngood): def test_jump_detect(): + """ + Create a simple ramp with a (2, 2) image that has a jump in two + different ramps and the computed slopes are still close. + """ nints, ngroups, nrows, ncols = 1, 10, 2, 2 rnval, gval = 10.0, 5.0 frame_time, nframes, groupgap = 10.736, 5, 2 @@ -611,6 +595,9 @@ def test_jump_detect(): tol = 1.e-4 assert abs(data[0, 0] - slope_est) < tol + assert abs(data[0, 1] - slope_est) < tol + assert abs(data[1, 0] - slope_est) < tol + assert abs(data[1, 1] - slope_est) < tol assert dq[0, 0] == JMP assert dq[0, 1] == GOOD assert dq[1, 0] == GOOD From afb3c65ced2c3387b7125816188ef513c5ae7734 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 14:20:27 -0400 Subject: [PATCH 37/63] Removing debugging import from ramp fitting likelihood. --- src/stcal/ramp_fitting/likely_fit.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index cb00708d..9dd8b973 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -15,15 +15,6 @@ from .likely_algo_classes import IntegInfo, Ramp_Result, Covar -################## DEBUG ################## -# HELP!! -import sys -sys.path.insert(1, "/Users/kmacdonald/code/common") -from general_funcs import dbg_print, \ - array_string -################## DEBUG ################## - - DELIM = "=" * 80 log = logging.getLogger(__name__) From 473cb6f293a5a324e7961c5bd17c669502b19225 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 7 Aug 2024 15:26:16 -0400 Subject: [PATCH 38/63] Updating the change log and cleaning up the code comments. --- src/stcal/ramp_fitting/likely_algo_classes.py | 1 - src/stcal/ramp_fitting/likely_fit.py | 39 +------------------ 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 07de82cc..c9ce644d 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -131,7 +131,6 @@ def fill_masked_reads(self, diffs2use): """ # replace entries that would be nan (from trying to # doubly exclude read differences) with the global fits. - # XXX Maybe here flag JUMP_DET omit = diffs2use == 0 ones = np.ones(diffs2use.shape) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 9dd8b973..2c01fb92 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -105,8 +105,6 @@ def likely_ramp_fit( diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=d2use ) - - # XXX SET JUMP_DET # Set jump detection flags jump_locs = d2use_copy ^ d2use jump_locs[jump_locs > 0] = ramp_data.flags_jump_det @@ -114,8 +112,6 @@ def likely_ramp_fit( alldiffs2use[:, row] = d2use # XXX May not be necessary - # XXX According to Brandt feedback - # rateguess = countrates * (countrates > 0) * darkrate (ramp_data.average_dark_current?) rateguess = countrates * (countrates > 0) + ramp_data.average_dark_current[row, :] result = fit_ramps( diff[:, row], @@ -223,7 +219,6 @@ def mask_jumps( # reasonably close to correct is important. countrateguess = np.median(loc_diff, axis=0)[np.newaxis, :] - # XXX Somehow add the Poisson variance back in. countrateguess *= countrateguess > 0 # boolean arrays to be used later @@ -264,7 +259,7 @@ def mask_jumps( best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) best_dchisq_two = np.amax( dchisq_two * twoomit_ok[:, np.newaxis], axis=0 - ) # XXX HERE Is this where JUMP_DET is set? + ) # Is the best improvement from dropping one resultant # difference or two? Two drops will always offer more @@ -392,29 +387,6 @@ def compute_image_info(integ_class, ramp_data): dq = utils.dq_compress_final(integ_class.dq, ramp_data) - # XXX Feedback from Brandt that this may not be correct. - # He provided another way to combine these computations. - """ - # print("**** Old Computations ****") - warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) - warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) - inv_vp = 1. / integ_class.var_poisson - var_p = 1. / inv_vp.sum(axis=0) - - inv_vr = 1. / integ_class.var_rnoise - var_r = 1. / inv_vr.sum(axis=0) - - inv_err = 1. / integ_class.err - err = 1. / inv_err.sum(axis=0) - - inv_err2 = 1. / (integ_class.err**2) - err2 = 1. / inv_err2.sum(axis=0) - - slope = integ_class.data * inv_err2 - slope = slope.sum(axis=0) * err2 - warnings.resetwarnings() - """ - # print("**** New Computations ****") inv_err2 = 1.0 / (integ_class.err**2) weight = inv_err2 / inv_err2.sum(axis=0) weight2 = weight**2 @@ -426,8 +398,6 @@ def compute_image_info(integ_class, ramp_data): var_r = np.sum(integ_class.var_rnoise * weight2, axis=0) slope = np.sum(integ_class.data * weight, axis=0) - # XXX Compute NaNs. - return (slope, dq, var_p, var_r, err) @@ -602,7 +572,6 @@ def fit_ramps( if countrateguess is None: countrateguess = inital_countrateguess(covar, diffs, diffs2use) - # XXX Maybe use a better name for this function, like compute_alphas_betas alpha_tuple, beta_tuple, scale = compute_alphas_betas( countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale ) @@ -748,8 +717,6 @@ def compute_jump_detects( if covar.pedestal: raise ValueError("Cannot use jump detection algorithm when fitting pedestals.") - # XXX need to determine where DQ flagging of JUMP_DET needs to occur. - # Diagonal elements of the inverse covariance matrix Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] Cinv_diag *= diffs2use @@ -825,7 +792,6 @@ def compute_jump_detects( result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) result.uncert_twoomit *= np.sqrt(scale) - # XXX Maybe this tells where to mask. result.fill_masked_reads(diffs2use) return result @@ -1299,15 +1265,12 @@ def get_ramp_result( warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) invC = 1 / C - # result.countrate = B / C result.countrate = B * invC result.chisq = (A - B**2 / C) / scale result.uncert = np.sqrt(scale / C) result.weights = dC / C - # XXX VAR - # alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) result.var_poisson += 2 * np.sum( result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0 From 702d47b618534c2ba79d4787cb134233de9871cb Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 9 Aug 2024 17:39:09 -0400 Subject: [PATCH 39/63] Updating link documents for ramp fitting likelihood algorithm description. --- docs/stcal/ramp_fitting/description.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index c515f630..a448e551 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -347,8 +347,9 @@ The covariance matrix :math:`C` is a tridiagonal matrix, due to the nature of th differences. Because the covariance matrix is tridiagonal, the computational complexity from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation and computations implemented, refer to -`Brandt (2024) `_. The Poisson and read noise -variance computations are based on equations (27) and (28), defining +`Brandt (2024) `_ and +`Brandt (2024) `_. +The Poisson and read noise computations are based on equations (27) and (28), defining :math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the off diagonal. This algorithm runs ramp fitting twice. The first run allows for a first From f67f5646e88081acc6e938dd46e3b19747802acf Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 9 Aug 2024 17:44:30 -0400 Subject: [PATCH 40/63] Updating docstring based on code review. --- src/stcal/ramp_fitting/likely_fit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2c01fb92..a4473863 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -362,7 +362,8 @@ def get_readtimes(ramp_data): def compute_image_info(integ_class, ramp_data): """ - Compute the diffs2use mask based on DQ flags of a row. + Compute all integrations into a single image of rates, + variances, and DQ flags. Parameters ---------- From f67e8eaf2a184dec0dce137671d3f5540626c2b1 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Mon, 12 Aug 2024 13:55:26 -0400 Subject: [PATCH 41/63] Adding parameter to the RampData class to be used in the likelihood algorithm. --- src/stcal/ramp_fitting/ramp_fit.py | 2 ++ src/stcal/ramp_fitting/ramp_fit_class.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index ad9803d0..7137dc03 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -97,6 +97,8 @@ def create_ramp_fit_class(model, algorithm, dqflags=None, suppress_one_group=Fal if hasattr(model.meta.exposure, "read_pattern"): ramp_data.read_pattern = [list(reads) for reads in model.meta.exposure.read_pattern] + # XXX If LIKELY, then make sure `nsig` gets set + ramp_data.set_dqflags(dqflags) ramp_data.start_row = 0 ramp_data.num_rows = ramp_data.data.shape[2] diff --git a/src/stcal/ramp_fitting/ramp_fit_class.py b/src/stcal/ramp_fitting/ramp_fit_class.py index e6c6fe3e..fb72b229 100644 --- a/src/stcal/ramp_fitting/ramp_fit_class.py +++ b/src/stcal/ramp_fitting/ramp_fit_class.py @@ -17,6 +17,7 @@ def __init__(self): # Meta information self.instrument_name = None self.read_pattern = None + self.nsig = None self.frame_time = None self.group_time = None From 453bf439630b3b95596790a8047afc7c77f131be Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Mon, 12 Aug 2024 13:55:52 -0400 Subject: [PATCH 42/63] Updating the likelihood algorithm based on code review. --- src/stcal/ramp_fitting/likely_fit.py | 49 +++++++++++++++++++--------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index a4473863..2b08a2e7 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -3,6 +3,7 @@ import logging import multiprocessing import time +import scipy import sys import warnings @@ -16,6 +17,7 @@ DELIM = "=" * 80 +SQRT2 = 1.41421356 log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -96,21 +98,36 @@ def likely_ramp_fit( # Eqn (5) diff = (data[1:] - data[:-1]) / covar.delta_t[:, np.newaxis, np.newaxis] - alldiffs2use = np.ones(diff.shape, np.uint8) # XXX May not be necessary + alldiffs2use = np.ones(diff.shape, np.uint8) for row in range(nrows): d2use = determine_diffs2use(ramp_data, integ, row, diff) d2use_copy = d2use.copy() # Use to flag jumps - d2use, countrates = mask_jumps( - diff[:, row], covar, readnoise_2d[row], gain_2d[row], diffs2use=d2use - ) + if ramp_data.nsig is not None: + threshold_oneomit = ramp_data.nsig**2 + # pval = scipy.special.erfc(ramp_data.nsig/2**0.5) + pval = scipy.special.erfc(ramp_data.nsig/SQRT2) + threshold_twoomit = scipy.stats.chi2.isf(pval, 2) + if np.isinf(threshold_twoomit): + threshold_twoomit = threshold_oneomit + 10 + d2use, countrates = mask_jumps( + diff[:, row], covar, readnoise_2d[row], gain_2d[row], + threshold_oneomit=threshold_oneomit, + threshold_twoomit=threshold_twoomit, + diffs2use=d2use + ) + else: + d2use, countrates = mask_jumps( + diff[:, row], covar, readnoise_2d[row], gain_2d[row], + diffs2use=d2use + ) # Set jump detection flags jump_locs = d2use_copy ^ d2use jump_locs[jump_locs > 0] = ramp_data.flags_jump_det gdq[1:, row, :] |= jump_locs - alldiffs2use[:, row] = d2use # XXX May not be necessary + alldiffs2use[:, row] = d2use rateguess = countrates * (countrates > 0) + ramp_data.average_dark_current[row, :] result = fit_ramps( @@ -388,16 +405,18 @@ def compute_image_info(integ_class, ramp_data): dq = utils.dq_compress_final(integ_class.dq, ramp_data) - inv_err2 = 1.0 / (integ_class.err**2) - weight = inv_err2 / inv_err2.sum(axis=0) - weight2 = weight**2 - - err2 = np.sum(integ_class.err**2 * weight2, axis=0) - - err = np.sqrt(err2) - var_p = np.sum(integ_class.var_poisson * weight2, axis=0) - var_r = np.sum(integ_class.var_rnoise * weight2, axis=0) - slope = np.sum(integ_class.data * weight, axis=0) + slope = np.median(integ_class.data, axis=0) + for _ in range(2): + rate_scale = slope[np.newaxis, :] / integ_class.data + rate_scale[(~np.isfinite(rate_scale)) | (rate_scale < 0)] = 0 + all_var_p = integ_class.var_poisson * rate_scale + weight = 1/(all_var_p + integ_class.var_rnoise) + weight /= np.sum(weight, axis=0)[np.newaxis, :] + slope = np.sum(integ_class.data*weight, axis=0) + + var_p = np.sum(all_var_p * weight**2, axis=0) + var_r = np.sum(integ_class.var_rnoise * weight**2, axis=0) + err = np.sqrt(var_p + var_r) return (slope, dq, var_p, var_r, err) From 647f5604f97e80a2a42fdef63baaf6b4b038b0f3 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 14 Aug 2024 17:23:10 -0400 Subject: [PATCH 43/63] Updating some comments and the usage of the readnoise array from CRDS. --- src/stcal/ramp_fitting/likely_fit.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2b08a2e7..df2ae7df 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -91,6 +91,8 @@ def likely_ramp_fit( covar = Covar(readtimes, pedestal=False) # XXX Choice of pedestal not given integ_class = IntegInfo(nints, nrows, ncols) + readnoise_2d = readnoise_2d / SQRT2 + for integ in range(nints): data = ramp_data.data[integ, :, :, :] gdq = ramp_data.groupdq[integ, :, :, :].copy() @@ -1226,10 +1228,10 @@ def get_ramp_result( Parameters ---------- - dB : ndarray + dC : ndarray Intermediate computation. - dC : ndarray + dB : ndarray Intermediate computation. A : ndarray @@ -1241,10 +1243,10 @@ def get_ramp_result( C : ndarray Intermediate computation. - rescale : boolean - Scale the covariance matrix internally to avoid possible + rescale : float + Factor applied to each element of the covariance matrix to + normalize its determinant in order to avoid possible overflow/underflow problems for long ramps. - Optional, default is True. phi : ndarray Intermediate computation. From 47758d117613cf046e600a6bf62e5b10413cab00 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 14 Aug 2024 17:23:43 -0400 Subject: [PATCH 44/63] Updating test due to change in read noise computation. --- tests/test_ramp_fitting_likly_fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index b0f35336..39d44940 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -367,7 +367,7 @@ def test_random_ramp(): data, dq, vp, vr, err = slopes tol = 1.e-4 - assert abs(data[0, 0] - 1.9972216) < tol + assert abs(data[0, 0] - 1.9960526) < tol assert dq[0, 0] == JMP assert abs(vp[0, 0] - 0.00064461) < tol assert abs(vr[0, 0] - 0.00018037) < tol From 2163be7444a5cb8a39bef7f67ad9b068fee4e77f Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 14 Aug 2024 17:28:00 -0400 Subject: [PATCH 45/63] Updating comments. --- src/stcal/ramp_fitting/likely_fit.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index df2ae7df..6349d74b 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -54,10 +54,6 @@ def likely_ramp_fit( gain_2d : ndarray gain for all pixels - algorithm : str - 'OLS' specifies that ordinary least squares should be used; - 'GLS' specifies that generalized least squares should be used. - weighting : str 'optimal' specifies that optimal weighting should be used; currently the only weighting supported. From 3ca30c183d97b057d5b5eadb3615e90db466114a Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 4 Sep 2024 13:51:24 -0400 Subject: [PATCH 46/63] Making updates based on code review. --- docs/stcal/ramp_fitting/description.rst | 25 +- src/stcal/ramp_fitting/likely_algo_classes.py | 74 +++--- src/stcal/ramp_fitting/likely_fit.py | 214 ++++++++---------- src/stcal/ramp_fitting/ramp_fit.py | 2 +- tests/test_ramp_fitting_likly_fit.py | 3 +- 5 files changed, 153 insertions(+), 165 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index a448e551..95141220 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -3,8 +3,8 @@ Description This step determines the mean count rate, in units of counts per second, for each pixel by performing a linear fit to the data in the input file. The default -is done using the "ordinary least squares" method using based on the Fixsen fitting -algorithm described by +method uses "ordinary least squares" based on the Fixsen fitting algorithm +described by `Fixsen et al. (2011) `_. The count rate for each pixel is determined by a linear fit to the @@ -18,9 +18,10 @@ saturation flags are found. Pixels are processed simultaneously in blocks using the array-based functionality of numpy. The size of the block depends on the image size and the number of groups. -There is a likelihood algorithm implemented based on Timoth Brandt's papers: +There is a likelihood algorithm implemented based on Timothy Brandt's papers: Optimal Fitting and Debiasing for Detectors Read Out Up-the-Ramp -Likelihood-Based Jump Detection and Cosmic Ray Rejection for Detectors Read Out Up-the-Ramp +Likelihood-Based Jump Detection and Cosmic Ray Rejection for Detectors Read Out Up-the-Ramp. +This algorithm is currently in beta phase as an alternative to the OLS method. .. _ramp_output_products: @@ -323,12 +324,12 @@ product. Likelihood Algorithm Details ---------------------------- As an alternative to the OLS algorithm, a likelihood algorithm can be selected -with for ``--ramp_fitting.algorithm=LIKELY``. If this algorithm is selected, -the normal pipeline jump detection algorithm is skipped because this algorithm -has its own jump detection algorithm. The jump detection for this algorithm -requires NGROUPS to be a minimum of four (4). If NGROUPS :math:`\le` 3, then -this algorithm is deselected, defaulting to the above described OLS algorithm -and the normal jump detection pipeline step is run. +with the step argument ``--ramp_fitting.algorithm=LIKELY``. If this algorithm +is selected, the normal pipeline jump detection algorithm is skipped because +this algorithm has its own jump detection algorithm. The jump detection for +this algorithm requires NGROUPS to be a minimum of four (4). If NGROUPS +:math:`\le` 3, then this algorithm is deselected, defaulting to the above +described OLS algorithm and the normal jump detection pipeline step is run. Each pixel is independently processed, but rather than operate on the each group/resultant directly, the likelihood algorithm is based on differences of @@ -345,10 +346,10 @@ Differentiating, setting to zero, then solving for :math:`a` results in The covariance matrix :math:`C` is a tridiagonal matrix, due to the nature of the differences. Because the covariance matrix is tridiagonal, the computational -complexity from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation +complexity reduces from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation and computations implemented, refer to `Brandt (2024) `_ and -`Brandt (2024) `_. +`Brandt (2024) `__. The Poisson and read noise computations are based on equations (27) and (28), defining :math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the off diagonal. diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index c9ce644d..70287ae4 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -43,7 +43,7 @@ def get_results(self, result, integ, row): Parameters ---------- - result : Ramp_Result + result : RampResult Holds computed ramp fitting information. integ : int @@ -58,7 +58,7 @@ def get_results(self, result, integ, row): self.var_rnoise[integ, row, :] = result.var_rnoise -class Ramp_Result: +class RampResult: def __init__(self): """ Contains the ramp fitting results. @@ -73,15 +73,15 @@ def __init__(self): self.uncert_pedestal = None self.covar_countrate_pedestal = None - self.countrate_twoomit = None - self.chisq_twoomit = None - self.uncert_twoomit = None + self.countrate_two_omit = None + self.chisq_two_omit = None + self.uncert_two_omit = None - self.countrate_oneomit = None - self.jumpval_oneomit = None - self.jumpsig_oneomit = None - self.chisq_oneomit = None - self.uncert_oneomit = None + self.countrate_one_omit = None + self.jumpval_one_omit = None + self.jumpsig_one_omit = None + self.chisq_one_omit = None + self.uncert_one_omit = None def __repr__(self): """ @@ -96,36 +96,38 @@ def __repr__(self): ostring += f"\nuncert_pedestal = \n{self.uncert_pedestal}" ostring += f"\ncovar_countrate_pedestal = \n{self.covar_countrate_pedestal}\n" - ostring += f"\ncountrate_twoomit = \n{self.countrate_twoomit}" - ostring += f"\nchisq_twoomit = \n{self.chisq_twoomit}" - ostring += f"\nuncert_twoomit = \n{self.uncert_twoomit}" + ostring += f"\ncountrate_two_omit = \n{self.countrate_two_omit}" + ostring += f"\nchisq_two_omit = \n{self.chisq_two_omit}" + ostring += f"\nuncert_two_omit = \n{self.uncert_two_omit}" - ostring += f"\ncountrate_oneomit = \n{self.countrate_oneomit}" - ostring += f"\njumpval_oneomit = \n{self.jumpval_oneomit}" - ostring += f"\njumpsig_oneomit = \n{self.jumpsig_oneomit}" - ostring += f"\nchisq_oneomit = \n{self.chisq_oneomit}" - ostring += f"\nuncert_oneomit = \n{self.uncert_oneomit}" + ostring += f"\ncountrate_one_omit = \n{self.countrate_one_omit}" + ostring += f"\njumpval_one_omit = \n{self.jumpval_one_omit}" + ostring += f"\njumpsig_one_omit = \n{self.jumpsig_one_omit}" + ostring += f"\nchisq_one_omit = \n{self.chisq_one_omit}" + ostring += f"\nuncert_one_omit = \n{self.uncert_one_omit}" ''' return ostring def fill_masked_reads(self, diffs2use): """ + Mask groups to use for ramp fitting. + Replace countrates, uncertainties, and chi squared values that are NaN because resultant differences were doubly omitted. For these cases, revert to the corresponding values in with fewer omitted resultant differences to get the correct values - without double-coundint omissions. + without double-counting omissions. This function replaces the relevant entries of - self.countrate_twoomit, self.chisq_twoomit, - self.uncert_twoomit, self.countrate_oneomit, and - self.chisq_oneomit in place. It does not return a value. + self.countrate_two_omit, self.chisq_two_omit, + self.uncert_two_omit, self.countrate_one_omit, and + self.chisq_one_omit in place. It does not return a value. Parameters ---------- diffs2use : ndarray - A 2D array matching self.countrate_oneomit in shape with zero + A 2D array matching self.countrate_one_omit in shape with zero for resultant differences that were masked and one for differences that were not masked. """ @@ -134,21 +136,21 @@ def fill_masked_reads(self, diffs2use): omit = diffs2use == 0 ones = np.ones(diffs2use.shape) - self.countrate_oneomit[omit] = (self.countrate * ones)[omit] - self.chisq_oneomit[omit] = (self.chisq * ones)[omit] - self.uncert_oneomit[omit] = (self.uncert * ones)[omit] + self.countrate_one_omit[omit] = (self.countrate * ones)[omit] + self.chisq_one_omit[omit] = (self.chisq * ones)[omit] + self.uncert_one_omit[omit] = (self.uncert * ones)[omit] omit = diffs2use[1:] == 0 - self.countrate_twoomit[omit] = (self.countrate_oneomit[:-1])[omit] - self.chisq_twoomit[omit] = (self.chisq_oneomit[:-1])[omit] - self.uncert_twoomit[omit] = (self.uncert_oneomit[:-1])[omit] + self.countrate_two_omit[omit] = (self.countrate_one_omit[:-1])[omit] + self.chisq_two_omit[omit] = (self.chisq_one_omit[:-1])[omit] + self.uncert_two_omit[omit] = (self.uncert_one_omit[:-1])[omit] omit = diffs2use[:-1] == 0 - self.countrate_twoomit[omit] = (self.countrate_oneomit[1:])[omit] - self.chisq_twoomit[omit] = (self.chisq_oneomit[1:])[omit] - self.uncert_twoomit[omit] = (self.uncert_oneomit[1:])[omit] + self.countrate_two_omit[omit] = (self.countrate_one_omit[1:])[omit] + self.chisq_two_omit[omit] = (self.chisq_one_omit[1:])[omit] + self.uncert_two_omit[omit] = (self.uncert_one_omit[1:])[omit] class Covar: @@ -245,7 +247,7 @@ def _compute_means_and_taus(self, readtimes, pedestal): def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): """ - Computes the means and taus of defined in EQNs 28 and 29 in paper 1. + Computes the means and taus defined in EQNs 28 and 29 in paper 1. Parameters ---------- @@ -269,7 +271,7 @@ def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): def _compute_pedestal(self, mean_t, tau, N, delta_t): """ - Computes the means and taus of defined in EQNs 28 and 29 in paper 1. + Computes the means and taus defined in EQNs 28 and 29 in paper 1. Parameters ---------- @@ -303,7 +305,7 @@ def _compute_pedestal(self, mean_t, tau, N, delta_t): def calc_bias(self, countrates, sig, cvec, da=1e-7): """ Calculate the bias in the best-fit count rate from estimating the - covariance matrix. This calculation is derived in the paper. + covariance matrix. Section 5 of paper 1. @@ -314,7 +316,7 @@ def calc_bias(self, countrates, sig, cvec, da=1e-7): Array of count rates at which the bias is desired. sig : float - Single read noise] + Single read noise. cvec : ndarray Weight vector on resultant differences for initial estimation diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 6349d74b..8d4e0f97 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -13,7 +13,7 @@ import numpy as np from . import ramp_fit_class, utils -from .likely_algo_classes import IntegInfo, Ramp_Result, Covar +from .likely_algo_classes import IntegInfo, RampResult, Covar DELIM = "=" * 80 @@ -28,10 +28,10 @@ log.setLevel(logging.DEBUG) -def likely_ramp_fit( - ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores -): +def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): """ + Invoke ramp fitting using the likelihood algorithm. + Setup the inputs to ols_ramp_fit with and without multiprocessing. The inputs will be sliced into the number of cores that are being used for multiprocessing. Because the data models cannot be pickled, only numpy @@ -42,28 +42,12 @@ def likely_ramp_fit( ramp_data : RampData Input data necessary for computing ramp fitting. - buffsize : int - size of data section (buffer) in bytes (not used) - - save_opt : bool - calculate optional fitting results - readnoise_2d : ndarray readnoise for all pixels gain_2d : ndarray gain for all pixels - weighting : str - 'optimal' specifies that optimal weighting should be used; - currently the only weighting supported. - - max_cores : str - Number of cores to use for multiprocessing. If set to 'none' (the default), - then no multiprocessing will be done. The other allowable values are 'quarter', - 'half', and 'all'. This is the fraction of cores to use for multi-proc. The - total number of cores includes the SMT cores (Hyper Threading for Intel). - Returns ------- image_info : tuple @@ -79,8 +63,8 @@ def likely_ramp_fit( nints, ngroups, nrows, ncols = ramp_data.data.shape - if ngroups < 2: - raise ValueError("Likelihood fit requires at least 2 groups.") + if ngroups < 4: + raise ValueError("Likelihood fit requires at least 4 groups.") readtimes = get_readtimes(ramp_data) @@ -102,16 +86,15 @@ def likely_ramp_fit( d2use = determine_diffs2use(ramp_data, integ, row, diff) d2use_copy = d2use.copy() # Use to flag jumps if ramp_data.nsig is not None: - threshold_oneomit = ramp_data.nsig**2 - # pval = scipy.special.erfc(ramp_data.nsig/2**0.5) + threshold_one_omit = ramp_data.nsig**2 pval = scipy.special.erfc(ramp_data.nsig/SQRT2) - threshold_twoomit = scipy.stats.chi2.isf(pval, 2) - if np.isinf(threshold_twoomit): - threshold_twoomit = threshold_oneomit + 10 + threshold_two_omit = scipy.stats.chi2.isf(pval, 2) + if np.isinf(threshold_two_omit): + threshold_two_omit = threshold_one_omit + 10 d2use, countrates = mask_jumps( diff[:, row], covar, readnoise_2d[row], gain_2d[row], - threshold_oneomit=threshold_oneomit, - threshold_twoomit=threshold_twoomit, + threshold_one_omit=threshold_one_omit, + threshold_two_omit=threshold_two_omit, diffs2use=d2use ) else: @@ -134,7 +117,7 @@ def likely_ramp_fit( gain_2d[row], readnoise_2d[row], diffs2use=d2use, - countrateguess=rateguess, + count_rate_guess=rateguess, ) integ_class.get_results(result, integ, row) @@ -151,11 +134,11 @@ def likely_ramp_fit( def mask_jumps( diffs, - Cov, + covar, rnoise, gain, - threshold_oneomit=20.25, - threshold_twoomit=23.8, + threshold_one_omit=20.25, + threshold_two_omit=23.8, diffs2use=None, ): @@ -169,8 +152,8 @@ def mask_jumps( The group differences of the data array for a given integration and row (ngroups-1, ncols). - Cov : Covar - The class that computes and contains the covariance matrix info. + covar : Covar + The class instance that computes and contains the covariance matrix info. rnoise : ndarray The read noise (ncols,) @@ -178,11 +161,11 @@ def mask_jumps( gain : ndarray The gain (ncols,) - threshold_oneomit : float + threshold_one_omit : float Minimum chisq improvement to exclude a single resultant difference. Default: 20.25. - threshold_twoomit : float + threshold_two_omit : float Minimum chisq improvement to exclude two sequential resultant differences. Default 23.8. @@ -201,7 +184,7 @@ def mask_jumps( Optional, default is None. """ - if Cov.pedestal: + if covar.pedestal: raise ValueError( "Cannot mask jumps with a Covar class that includes a pedestal fit." ) @@ -214,13 +197,13 @@ def mask_jumps( # pattern has more than one read per resultant but significant # gaps between resultants then a one-omit search might still be a # good idea even with multiple-read resultants. - oneomit_ok = Cov.Nreads[1:] * Cov.Nreads[:-1] >= 1 - oneomit_ok[0] = oneomit_ok[-1] = True + one_omit_ok = covar.Nreads[1:] * covar.Nreads[:-1] >= 1 + one_omit_ok[0] = one_omit_ok[-1] = True # Other than that, we need to omit two. If a resultant has more # than two reads, we need to omit both differences containing it # (one pair of omissions in the differences). - twoomit_ok = Cov.Nreads[1:-1] > 1 + two_omit_ok = covar.Nreads[1:-1] > 1 # This is the array to return: one for resultant differences to # use, zero for resultant differences to ignore. @@ -232,9 +215,9 @@ def mask_jumps( # jumps (which is what we are looking for) since we'll be using # likelihoods and chi squared; getting the covariance matrix # reasonably close to correct is important. - countrateguess = np.median(loc_diff, axis=0)[np.newaxis, :] + count_rate_guess = np.median(loc_diff, axis=0)[np.newaxis, :] - countrateguess *= countrateguess > 0 + count_rate_guess *= count_rate_guess > 0 # boolean arrays to be used later recheck = np.ones(loc_diff.shape[1]) == 1 @@ -245,10 +228,10 @@ def mask_jumps( if j == 0: result = fit_ramps( loc_diff, - Cov, + covar, gain, rnoise, - countrateguess=countrateguess, + count_rate_guess=count_rate_guess, diffs2use=diffs2use, detect_jumps=True, ) @@ -258,22 +241,22 @@ def mask_jumps( else: result = fit_ramps( loc_diff[:, recheck], - Cov, + covar, gain[recheck], rnoise[recheck], - countrateguess=countrateguess[:, recheck], + count_rate_guess=count_rate_guess[:, recheck], diffs2use=diffs2use[:, recheck], detect_jumps=True, ) # Chi squared improvements - dchisq_two = result.chisq - result.chisq_twoomit - dchisq_one = result.chisq - result.chisq_oneomit + dchisq_two = result.chisq - result.chisq_two_omit + dchisq_one = result.chisq - result.chisq_one_omit # We want the largest chi squared difference - best_dchisq_one = np.amax(dchisq_one * oneomit_ok[:, np.newaxis], axis=0) + best_dchisq_one = np.amax(dchisq_one * one_omit_ok[:, np.newaxis], axis=0) best_dchisq_two = np.amax( - dchisq_two * twoomit_ok[:, np.newaxis], axis=0 + dchisq_two * two_omit_ok[:, np.newaxis], axis=0 ) # Is the best improvement from dropping one resultant @@ -283,14 +266,14 @@ def mask_jumps( # corresponding to dropping either one or two reads, whichever # is better, if either exceeded the threshold. onedropbetter = ( - best_dchisq_one - threshold_oneomit > best_dchisq_two - threshold_twoomit + best_dchisq_one - threshold_one_omit > best_dchisq_two - threshold_two_omit ) best_dchisq = ( - best_dchisq_one * (best_dchisq_one > threshold_oneomit) * onedropbetter + best_dchisq_one * (best_dchisq_one > threshold_one_omit) * onedropbetter ) best_dchisq += ( - best_dchisq_two * (best_dchisq_two > threshold_twoomit) * (~onedropbetter) + best_dchisq_two * (best_dchisq_two > threshold_two_omit) * (~onedropbetter) ) # If nothing exceeded the threshold set the improvement to @@ -311,9 +294,9 @@ def mask_jumps( # Store the updated counts with omitted reads new_cts = np.zeros(np.sum(recheck)) i_d1 = np.sum(dropone, axis=0) > 0 - new_cts[i_d1] = np.sum(result.countrate_oneomit * dropone, axis=0)[i_d1] + new_cts[i_d1] = np.sum(result.countrate_one_omit * dropone, axis=0)[i_d1] i_d2 = np.sum(droptwo, axis=0) > 0 - new_cts[i_d2] = np.sum(result.countrate_twoomit * droptwo, axis=0)[i_d2] + new_cts[i_d2] = np.sum(result.countrate_two_omit * droptwo, axis=0)[i_d2] # zero out count rates with drops and add their new values back in countrate[recheck] *= drop == 0 @@ -342,13 +325,14 @@ def mask_jumps( def get_readtimes(ramp_data): """ - Get the read times needed to compute the covariance matrices. If there is - already a read_pattern in the ramp_data class, then just get it. If not, then - one needs to be constructed. If one needs to be constructed it is assumed the - groups are evenly spaced in time, as are the frames that make up the group. If - each group has only one frame and no group gap, then a list of the group times - is returned. If nframes > 0, then a list of lists of each frame time in each - group is returned with the assumption: + Get the read times needed to compute the covariance matrices. + + If there is already a read_pattern in the ramp_data class, then just get it. + If not, then one needs to be constructed. If one needs to be constructed it + is assumed the groups are evenly spaced in time, as are the frames that make + up the group. If each group has only one frame and no group gap, then a list + of the group times is returned. If nframes > 0, then a list of lists of each + frame time in each group is returned with the assumption: group_time = (nframes + groupgap) * frame_time Parameters @@ -365,7 +349,6 @@ def get_readtimes(ramp_data): return ramp_data.read_pattern ngroups = ramp_data.data.shape[1] - # rtimes = [(k + 1) * ramp_data.group_time for k in range(ngroups)] # XXX Old tot_frames = ramp_data.nframes + ramp_data.groupgap tot_nreads = np.arange(1, ramp_data.nframes + 1) rtimes = [ @@ -377,7 +360,7 @@ def get_readtimes(ramp_data): def compute_image_info(integ_class, ramp_data): """ - Compute all integrations into a single image of rates, + Combine all integrations into a single image of rates, variances, and DQ flags. Parameters @@ -480,14 +463,14 @@ def determine_diffs2use(ramp_data, integ, row, diffs): return d2use -def inital_countrateguess(covar, diffs, diffs2use): +def inital_count_rate_guess(covar, diffs, diffs2use): """ Compute the initial count rate. Parameters ---------- covar : Covar - The class that computes and contains the covariance matrix info. + The class instance that computes and contains the covariance matrix info. diffs : ndarray The group differences of the data (ngroups-1, nrows, ncols). @@ -497,7 +480,7 @@ def inital_countrateguess(covar, diffs, diffs2use): Returns ------- - countrateguess : ndarray + count_rate_guess : ndarray The initial count rate. """ # initial guess for count rate is the average of the unmasked @@ -509,10 +492,10 @@ def inital_countrateguess(covar, diffs, diffs2use): num = np.sum((diffs * diffs2use), axis=0) den = np.sum(diffs2use, axis=0) - countrateguess = num / den - countrateguess *= countrateguess > 0 + count_rate_guess = num / den + count_rate_guess *= count_rate_guess > 0 - return countrateguess + return count_rate_guess # RAMP FITTING BEGIN @@ -521,7 +504,7 @@ def fit_ramps( covar, gain, rnoise, # Referred to as 'sig' in fitramp repo - countrateguess=None, + count_rate_guess=None, diffs2use=None, detect_jumps=False, resetval=0, @@ -530,9 +513,11 @@ def fit_ramps( dn_scale=10.0, ): """ - Function fit_ramps on a row of pixels. Fits ramps to read differences - using the covariance matrix for the read differences as given by the - diagonal elements and the off-diagonal elements. + Fit ramps on a row of data. + + Fits ramps to read differences using the covariance matrix for + the read differences as given by the diagonal elements and the + off-diagonal elements. Parameters ---------- @@ -540,7 +525,7 @@ def fit_ramps( The group differences of the data (ngroups-1, nrows, ncols). covar : Covar - The class that computes and contains the covariance matrix info. + The class instance that computes and contains the covariance matrix info. gain : ndarray The gain (ncols,) @@ -548,7 +533,7 @@ def fit_ramps( rnoise : ndarray The read noise (ncols,) - countrateguess : ndarray + count_rate_guess : ndarray Count rate estimates used to estimate the covariance matrix. Optional, default is None. @@ -574,12 +559,12 @@ def fit_ramps( overflow/underflow problems for long ramps. Optional, default is True. - dn_scale : XXX - XXX + dn_scale : float + Scaling factor. Returns ------- - result : Ramp_Result + result : RampResult Holds computed ramp fitting information. XXX - rename """ if diffs2use is None: @@ -587,11 +572,11 @@ def fit_ramps( diffs2use = np.ones(diffs.shape, np.uint8) # diffs is (ngroups, ncols) of the current row - if countrateguess is None: - countrateguess = inital_countrateguess(covar, diffs, diffs2use) + if count_rate_guess is None: + count_rate_guess = inital_count_rate_guess(covar, diffs, diffs2use) alpha_tuple, beta_tuple, scale = compute_alphas_betas( - countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale + count_rate_guess, gain, rnoise, covar, rescale, diffs, dn_scale ) alpha, alpha_phnoise, alpha_readnoise = alpha_tuple beta, beta_phnoise, beta_readnoise = beta_tuple @@ -667,6 +652,8 @@ def compute_jump_detects( result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar ): """ + Detect jumps in ramps. + The code below computes the best chi squared, best-fit slope, and its uncertainty leaving out each group difference in turn. There are ndiffs possible differences that can be @@ -685,7 +672,7 @@ def compute_jump_detects( Parameters ---------- - result : Ramp_Result + result : RampResult The results of the ramp fitting for a given row of pixels in an integration. ndiffs : int @@ -722,12 +709,12 @@ def compute_jump_detects( Intermediate computation. covar : Covar - The class that computes and contains the covariance matrix info. + The class instance that computes and contains the covariance matrix info. Returns ------- - result : Ramp_Result + result : RampResult The results of the ramp fitting for a given row of pixels in an integration. """ # The algorithms below do not work if we are computing the @@ -754,11 +741,11 @@ def compute_jump_detects( a = (Cinv_diag * B - dB * dC) / (C * Cinv_diag - dC**2) b = (dB - a * dC) / Cinv_diag - result.countrate_oneomit = a - result.jumpval_oneomit = b + result.countrate_one_omit = a + result.jumpval_one_omit = b # Use the best-fit a, b to get chi squared - result.chisq_oneomit = ( + result.chisq_one_omit = ( A + a**2 * C - 2 * a * B @@ -768,12 +755,12 @@ def compute_jump_detects( ) # invert the covariance matrix of a, b to get the uncertainty on a - result.uncert_oneomit = np.sqrt(Cinv_diag / (C * Cinv_diag - dC**2)) - result.jumpsig_oneomit = np.sqrt(C / (C * Cinv_diag - dC**2)) + result.uncert_one_omit = np.sqrt(Cinv_diag / (C * Cinv_diag - dC**2)) + result.jumpsig_one_omit = np.sqrt(C / (C * Cinv_diag - dC**2)) - result.chisq_oneomit /= scale - result.uncert_oneomit *= np.sqrt(scale) - result.jumpsig_oneomit *= np.sqrt(scale) + result.chisq_one_omit /= scale + result.uncert_one_omit *= np.sqrt(scale) + result.jumpsig_one_omit *= np.sqrt(scale) # Now for two omissions in a row. This is more work. Again, # all equations are in the paper. I first define three @@ -790,40 +777,40 @@ def compute_jump_detects( c /= cjck_fac / cpj_fac - (dC[1:] ** 2 - C * Cinv_diag[1:]) / cjck_fac b = (bcpj_fac - c * cjck_fac) / cpj_fac a = (B - b * dC[:-1] - c * dC[1:]) / C - result.countrate_twoomit = a + result.countrate_two_omit = a # best-fit chi squared - result.chisq_twoomit = ( + result.chisq_two_omit = ( A + a**2 * C + b**2 * Cinv_diag[:-1] + c**2 * Cinv_diag[1:] ) - result.chisq_twoomit -= 2 * a * B + 2 * b * dB[:-1] + 2 * c * dB[1:] - result.chisq_twoomit += ( + result.chisq_two_omit -= 2 * a * B + 2 * b * dB[:-1] + 2 * c * dB[1:] + result.chisq_two_omit += ( 2 * a * b * dC[:-1] + 2 * a * c * dC[1:] + 2 * b * c * Cinv_offdiag ) - result.chisq_twoomit /= scale + result.chisq_two_omit /= scale # uncertainty on the slope from inverting the (a, b, c) # covariance matrix fac = Cinv_diag[1:] * Cinv_diag[:-1] - Cinv_offdiag**2 term2 = dC[:-1] * (dC[:-1] * Cinv_diag[1:] - Cinv_offdiag * dC[1:]) term3 = dC[1:] * (dC[:-1] * Cinv_offdiag - Cinv_diag[:-1] * dC[1:]) - result.uncert_twoomit = np.sqrt(fac / (C * fac - term2 + term3)) - result.uncert_twoomit *= np.sqrt(scale) + result.uncert_two_omit = np.sqrt(fac / (C * fac - term2 + term3)) + result.uncert_two_omit *= np.sqrt(scale) result.fill_masked_reads(diffs2use) return result -def compute_alphas_betas(countrateguess, gain, rnoise, covar, rescale, diffs, dn_scale): +def compute_alphas_betas(count_rate_guess, gain, rnoise, covar, rescale, diffs, dn_scale): """ Compute alpha, beta, and scale needed for ramp fit. + Elements of the covariance matrix. - Are these EQNs 32 and 33? Parameters ---------- - countrateguess : ndarray + count_rate_guess : ndarray Initial guess (ncols,) gain : ndarray @@ -833,7 +820,7 @@ def compute_alphas_betas(countrateguess, gain, rnoise, covar, rescale, diffs, dn Read noise (ncols,) covar : Covar - The class that computes and contains the covariance matrix info. + The class instance that computes and contains the covariance matrix info. rescale : bool Determination to rescale covariance matrix. @@ -841,8 +828,8 @@ def compute_alphas_betas(countrateguess, gain, rnoise, covar, rescale, diffs, dn diffs : ndarray The group differences of the data (ngroups-1, nrows, ncols). - dn_scale : XXX - XXX + dn_scale : float + Scaling factor. Returns ------- @@ -859,13 +846,11 @@ def compute_alphas_betas(countrateguess, gain, rnoise, covar, rescale, diffs, dn warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) - # import ipdb; ipdb.set_trace() - - alpha_phnoise = countrateguess / gain * covar.alpha_phnoise[:, np.newaxis] + alpha_phnoise = count_rate_guess / gain * covar.alpha_phnoise[:, np.newaxis] alpha_readnoise = rnoise**2 * covar.alpha_readnoise[:, np.newaxis] alpha = alpha_phnoise + alpha_readnoise - beta_phnoise = countrateguess / gain * covar.beta_phnoise[:, np.newaxis] + beta_phnoise = count_rate_guess / gain * covar.beta_phnoise[:, np.newaxis] beta_readnoise = rnoise**2 * covar.beta_readnoise[:, np.newaxis] beta = beta_phnoise + beta_readnoise @@ -1121,6 +1106,7 @@ def matrix_computations( ): """ Computing matrix computations needed for ramp fitting. + EQNs 61-63, 71, 75 Parameters @@ -1251,7 +1237,7 @@ def get_ramp_result( Intermediate computation. covar : Covar - The class that computes and contains the covariance matrix info. + The class instance that computes and contains the covariance matrix info. resetval : float or ndarray Priors on the reset values. Irrelevant unless pedestal is True. If an @@ -1269,10 +1255,10 @@ def get_ramp_result( Returns ------- - result : Ramp_Result + result : RampResult The results of the ramp fitting for a given row of pixels in an integration. """ - result = Ramp_Result() + result = RampResult() # Finally, save the best-fit count rate, chi squared, uncertainty # in the count rate, and the weights used to combine the diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index 7137dc03..adf7c9ea 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -273,7 +273,7 @@ def ramp_fit_data( opt_info = None elif algorithm.upper() == "LIKELY" and ngroups >= likely_min_ngroups: image_info, integ_info, opt_info = likely_fit.likely_ramp_fit( - ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, weighting, max_cores + ramp_data, readnoise_2d, gain_2d ) gls_opt_info = None else: diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likly_fit.py index 39d44940..7bd1fb00 100644 --- a/tests/test_ramp_fitting_likly_fit.py +++ b/tests/test_ramp_fitting_likly_fit.py @@ -441,10 +441,9 @@ def test_too_few_group_ramp(ngroups): ramp = np.array(list(range(ngroups))) * 20 + 10 ramp_data.data[0, :, 0, 0] = ramp - save_opt, algo, ncores = False, "LIKELY", "none" with pytest.raises(ValueError): image_info, integ_info, opt_info = likely_ramp_fit( - ramp_data, 512, save_opt, rnoise2d, gain2d, "optimal", ncores + ramp_data, rnoise2d, gain2d ) From 383d267be91cb359ee3266f8eb8b76c8c8216ee8 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 4 Sep 2024 13:56:06 -0400 Subject: [PATCH 47/63] Adding change log fragment for towncrier. --- changes/278.apichange.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/278.apichange.rst diff --git a/changes/278.apichange.rst b/changes/278.apichange.rst new file mode 100644 index 00000000..e847fc7b --- /dev/null +++ b/changes/278.apichange.rst @@ -0,0 +1 @@ +[ramp_fitting] Add the likelihood algorithm to ramp fitting. From 5276b88fbb82699ed97387bc3f7f905545bb0219 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 5 Sep 2024 07:41:43 -0400 Subject: [PATCH 48/63] Removing pedestal related code. --- src/stcal/ramp_fitting/likely_algo_classes.py | 65 +--------- src/stcal/ramp_fitting/likely_fit.py | 121 ++++-------------- 2 files changed, 27 insertions(+), 159 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 70287ae4..261b4385 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -69,9 +69,6 @@ def __init__(self): self.var_poisson = None self.var_rnoise = None self.weights = None - self.pedestal = None - self.uncert_pedestal = None - self.covar_countrate_pedestal = None self.countrate_two_omit = None self.chisq_two_omit = None @@ -92,9 +89,6 @@ def __repr__(self): ostring += f"\nucert = \n{self.uncert}" ''' ostring += f"\nweights = \n{self.weights}" - ostring += f"\npedestal = \n{self.pedestal}" - ostring += f"\nuncert_pedestal = \n{self.uncert_pedestal}" - ostring += f"\ncovar_countrate_pedestal = \n{self.covar_countrate_pedestal}\n" ostring += f"\ncountrate_two_omit = \n{self.countrate_two_omit}" ostring += f"\nchisq_two_omit = \n{self.chisq_two_omit}" @@ -158,7 +152,7 @@ class Covar: class Covar holding read and photon noise components of alpha and beta and the time intervals between the resultant midpoints """ - def __init__(self, readtimes, pedestal=False): + def __init__(self, readtimes): """ Compute alpha and beta, the diagonal and off-diagonal elements of the covariance matrix of the resultant differences, and the time @@ -170,16 +164,10 @@ def __init__(self, readtimes, pedestal=False): List of values or lists for the times of reads. If a list of lists, times for reads that are averaged together to produce a resultant. - - pedestal : boolean - Does the covariance matrix include the terms for the first - resultant? This is needed if fitting for the pedestal (i.e. - the reset value). Optional parameter Default: False. """ # Equations (4) and (11) in paper 1. - mean_t, tau, N, delta_t = self._compute_means_and_taus(readtimes, pedestal) + mean_t, tau, N, delta_t = self._compute_means_and_taus(readtimes) - self.pedestal = pedestal self.delta_t = delta_t self.mean_t = mean_t self.tau = tau @@ -188,11 +176,7 @@ def __init__(self, readtimes, pedestal=False): # Equations (28) and (29) in paper 1. self._compute_alphas_and_betas(mean_t, tau, N, delta_t) - if pedestal: - # Equations (32) and (33) in paper 1. - self._compute_pedestal(mean_t, tau, N, delta_t) - - def _compute_means_and_taus(self, readtimes, pedestal): + def _compute_means_and_taus(self, readtimes): """ Computes the means and taus of defined in EQNs 4 and 11 in paper 1. @@ -202,11 +186,6 @@ def _compute_means_and_taus(self, readtimes, pedestal): List of values or lists for the times of reads. If a list of lists, times for reads that are averaged together to produce a resultant. - - pedestal : boolean - Does the covariance matrix include the terms for the first - resultant? This is needed if fitting for the pedestal (i.e. - the reset value). """ mean_t = [] # mean time of the resultant as defined in the paper tau = [] # variance-weighted mean time of the resultant @@ -269,39 +248,6 @@ def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): self.alpha_phnoise = (tau[:-1] + tau[1:] - 2 * mean_t[:-1]) / delta_t**2 self.beta_phnoise = (mean_t[1:-1] - tau[1:-1]) / (delta_t[1:] * delta_t[:-1]) - def _compute_pedestal(self, mean_t, tau, N, delta_t): - """ - Computes the means and taus defined in EQNs 28 and 29 in paper 1. - - Parameters - ---------- - mean_t : ndarray - The means of the reads for each group. - - tau : ndarray - Intermediate computation. - - N : ndarray - The number of reads in each group. - - delta_t : ndarray - The group differences of integration ramps. - """ - # If we want the reset value we need to include the first - # resultant. These are the components of the variance and - # covariance for the first resultant. - arn = list(self.alpha_readnoise) - brn = list(self.beta_readnoise) - ahn = list(self.alpha_phnoise) - bhn = list(self.beta_phnoise) - - self.alpha_readnoise = np.array([1 / (N[0] * mean_t[0] ** 2)] + arn) - self.beta_readnoise = np.array([-1 / (N[0] * mean_t[0] * delta_t[0])] + brn) - self.alpha_phnoise = np.array([tau[0] / mean_t[0] ** 2] + ahn) - self.beta_phnoise = np.array( - [(mean_t[0] - tau[0]) / (mean_t[0] * delta_t[0])] + bhn - ) - def calc_bias(self, countrates, sig, cvec, da=1e-7): """ Calculate the bias in the best-fit count rate from estimating the @@ -333,11 +279,6 @@ def calc_bias(self, countrates, sig, cvec, da=1e-7): Bias of the best-fit count rate from using cvec plus the observed resultants to estimate the covariance matrix. """ - if self.pedestal: - raise ValueError( - "Cannot compute bias with a Covar class that includes a pedestal fit." - ) - alpha = countrates[np.newaxis, :] * self.alpha_phnoise[:, np.newaxis] alpha += sig**2 * self.alpha_readnoise[:, np.newaxis] beta = countrates[np.newaxis, :] * self.beta_phnoise[:, np.newaxis] diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 8d4e0f97..eeb8e11d 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -68,7 +68,7 @@ def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): readtimes = get_readtimes(ramp_data) - covar = Covar(readtimes, pedestal=False) # XXX Choice of pedestal not given + covar = Covar(readtimes) integ_class = IntegInfo(nints, nrows, ncols) readnoise_2d = readnoise_2d / SQRT2 @@ -184,11 +184,6 @@ def mask_jumps( Optional, default is None. """ - if covar.pedestal: - raise ValueError( - "Cannot mask jumps with a Covar class that includes a pedestal fit." - ) - # Force a copy of the input array for more efficient memory access. loc_diff = diffs * 1 @@ -485,12 +480,8 @@ def inital_count_rate_guess(covar, diffs, diffs2use): """ # initial guess for count rate is the average of the unmasked # group differences unless otherwise specified. - if covar.pedestal: - num = np.sum((diffs * diffs2use)[1:], axis=0) - den = np.sum(diffs2use[1:], axis=0) - else: - num = np.sum((diffs * diffs2use), axis=0) - den = np.sum(diffs2use, axis=0) + num = np.sum((diffs * diffs2use), axis=0) + den = np.sum(diffs2use, axis=0) count_rate_guess = num / den count_rate_guess *= count_rate_guess > 0 @@ -507,8 +498,6 @@ def fit_ramps( count_rate_guess=None, diffs2use=None, detect_jumps=False, - resetval=0, - resetsig=np.inf, rescale=True, dn_scale=10.0, ): @@ -545,15 +534,6 @@ def fit_ramps( Run jump detection. Optional, default is False. - resetval : float or ndarray - Priors on the reset values. Irrelevant unless pedestal is True. If an - ndarray, it has dimensions (ncols). - Opfional, default is 0. - - resetsig : float or ndarray - Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. - Optional, default np.inf, i.e., reset values have flat priors. - rescale : boolean Scale the covariance matrix internally to avoid possible overflow/underflow problems for long ramps. @@ -627,8 +607,6 @@ def fit_ramps( phi, theta, covar, - resetval, - resetsig, alpha_phnoise, alpha_readnoise, beta_phnoise, @@ -662,12 +640,6 @@ def compute_jump_detects( Then do it omitting two consecutive reads. There are ndiffs-1 possible pairs of adjacent reads that can be omitted. - This approach would need to be modified if also fitting the - pedestal, so that condition currently triggers an error. The - modifications would make the equations significantly more - complicated; the matrix equations to be solved by hand would be - larger. - Paper II, sections 3.1 and 3.2 Parameters @@ -717,11 +689,6 @@ def compute_jump_detects( result : RampResult The results of the ramp fitting for a given row of pixels in an integration. """ - # The algorithms below do not work if we are computing the - # pedestal here. - if covar.pedestal: - raise ValueError("Cannot use jump detection algorithm when fitting pedestals.") - # Diagonal elements of the inverse covariance matrix Cinv_diag = theta[:-1] * phi[1:] / theta[ndiffs] Cinv_diag *= diffs2use @@ -1001,8 +968,7 @@ def compute_Phis(ndiffs, npix, beta, phi, sgn): def compute_PhiDs(ndiffs, npix, beta, phi, sgn, diff_mask): """ EQN 4, Paper II - This one is defined later in the paper and is used for jump - detection and pedestal fitting. + This one is defined later in the paper and is used for jump detection. Parameters ---------- @@ -1198,8 +1164,6 @@ def get_ramp_result( phi, theta, covar, - resetval, - resetsig, alpha_phnoise, alpha_readnoise, beta_phnoise, @@ -1239,19 +1203,10 @@ def get_ramp_result( covar : Covar The class instance that computes and contains the covariance matrix info. - resetval : float or ndarray - Priors on the reset values. Irrelevant unless pedestal is True. If an - ndarray, it has dimensions (ncols). - Opfional, default is 0. - - resetsig : float or ndarray - Uncertainties on the reset values. Irrelevant unless covar.pedestal is True. - Optional, default np.inf, i.e., reset values have flat priors. - - alpha_phnoise : - alpha_readnoise : - beta_phnoise : - beta_readnoise : + alpha_phnoise : XXX + alpha_readnoise : XXX + beta_phnoise : XXX + beta_readnoise : XXX Returns ------- @@ -1264,54 +1219,26 @@ def get_ramp_result( # in the count rate, and the weights used to combine the # groups. - # XXX pedestal is always False. - if not covar.pedestal: - warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) - warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) - invC = 1 / C - result.countrate = B * invC - result.chisq = (A - B**2 / C) / scale - - result.uncert = np.sqrt(scale / C) - result.weights = dC / C - - result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) - result.var_poisson += 2 * np.sum( - result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0 - ) + warnings.filterwarnings("ignore", ".*invalid value.*", RuntimeWarning) + warnings.filterwarnings("ignore", ".*divide by zero.*", RuntimeWarning) + invC = 1 / C + result.countrate = B * invC + result.chisq = (A - B**2 / C) / scale - result.var_rnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) - result.var_rnoise += 2 * np.sum( - result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0 - ) + result.uncert = np.sqrt(scale / C) + result.weights = dC / C - warnings.resetwarnings() + result.var_poisson = np.sum(result.weights**2 * alpha_phnoise, axis=0) + result.var_poisson += 2 * np.sum( + result.weights[1:] * result.weights[:-1] * beta_phnoise, axis=0 + ) - # If we are computing the pedestal, then we use the other formulas - # in the paper. + result.var_rnoise = np.sum(result.weights**2 * alpha_readnoise, axis=0) + result.var_rnoise += 2 * np.sum( + result.weights[1:] * result.weights[:-1] * beta_readnoise, axis=0 + ) - else: - dt = covar.mean_t[0] - Cinv_11 = theta[0] * phi[1] / theta[ndiffs] - - # Calculate the pedestal and slope using the equations in the paper. - # Do not compute weights for this case. - - b = dB[0] * C * dt - B * dC[0] * dt + dt**2 * C * resetval / resetsig**2 - b /= C * Cinv_11 - dC[0] ** 2 + dt**2 * C / resetsig**2 - a = B / C - b * dC[0] / C / dt - result.pedestal = b - result.countrate = a - result.chisq = A + a**2 * C + b**2 / dt**2 * Cinv_11 - result.chisq += -2 * b / dt * dB[0] - 2 * a * B + 2 * a * b / dt * dC[0] - result.chisq /= scale - - # elements of the inverse covariance matrix - M = [C, dC[0] / dt, Cinv_11 / dt**2 + 1 / resetsig**2] - detM = M[0] * M[-1] - M[1] ** 2 - result.uncert = np.sqrt(scale * M[-1] / detM) - result.uncert_pedestal = np.sqrt(scale * M[0] / detM) - result.covar_countrate_pedestal = -scale * M[1] / detM + warnings.resetwarnings() return result From 9f114786ff6fb550e10edfa45968e4f5bab35e7d Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 5 Sep 2024 07:56:26 -0400 Subject: [PATCH 49/63] Renaming file. --- ..._ramp_fitting_likly_fit.py => test_ramp_fitting_likely_fit.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_ramp_fitting_likly_fit.py => test_ramp_fitting_likely_fit.py} (100%) diff --git a/tests/test_ramp_fitting_likly_fit.py b/tests/test_ramp_fitting_likely_fit.py similarity index 100% rename from tests/test_ramp_fitting_likly_fit.py rename to tests/test_ramp_fitting_likely_fit.py From a3f3a89983f3be149c2d564fbcb339500b740edf Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 5 Sep 2024 07:57:48 -0400 Subject: [PATCH 50/63] Changes made due to code review. --- src/stcal/ramp_fitting/likely_fit.py | 23 ++++++++++++++++++----- src/stcal/ramp_fitting/ramp_fit.py | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index eeb8e11d..4fa4cba7 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -878,6 +878,8 @@ def compute_alphas_betas(count_rate_guess, gain, rnoise, covar, rescale, diffs, def compute_thetas(ndiffs, npix, alpha, beta): """ + Computes intermediate theta values for ramp fitting. + EQNs 38-40 Parameters @@ -907,6 +909,8 @@ def compute_thetas(ndiffs, npix, alpha, beta): def compute_phis(ndiffs, npix, alpha, beta): """ + Computes intermediate phi values for ramp fitting. + EQNs 41-43 Parameters @@ -936,6 +940,8 @@ def compute_phis(ndiffs, npix, alpha, beta): def compute_Phis(ndiffs, npix, beta, phi, sgn): """ + Computes intermediate Phi values for ramp fitting. + EQN 46 Parameters @@ -1071,7 +1077,7 @@ def matrix_computations( ndiffs, npix, sgn, diff_mask, diffs2use, beta, phi, Phi, PhiD, theta, Theta, ThetaD ): """ - Computing matrix computations needed for ramp fitting. + Compute matrix computations needed for ramp fitting. EQNs 61-63, 71, 75 @@ -1203,10 +1209,17 @@ def get_ramp_result( covar : Covar The class instance that computes and contains the covariance matrix info. - alpha_phnoise : XXX - alpha_readnoise : XXX - beta_phnoise : XXX - beta_readnoise : XXX + alpha_phnoise : ndarray + The photon noise contribution to the alphas. + + alpha_readnoise : ndarray + The read noise contribution to the alphas. + + beta_phnoise : ndarray + The photon noise contribution to the betas. + + beta_readnoise : ndarray + The read noise contribution to the betas. Returns ------- diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index adf7c9ea..bd6a1418 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -280,7 +280,7 @@ def ramp_fit_data( # Default to OLS. # Get readnoise array for calculation of variance of noiseless ramps, and # gain array in case optimal weighting is to be done - # XXX If the LIKELY is selected, log that the "OLS" algorithm is being use + # If the LIKELY is selected, log that the "OLS" algorithm is being use # and note the minimum number of ngroups needed. if algorithm.upper() == "LIKELY" and ngroups < likely_min_ngroups: msg = f"The 'OLS' algorithm is used since the LIKELY algorithm requires {likely_min_ngroups} or more" From 151f769f8eb930826ae7bc47f81d552b9369b600 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 5 Sep 2024 08:49:18 -0400 Subject: [PATCH 51/63] Updating due to code review. --- src/stcal/ramp_fitting/ramp_fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index bd6a1418..ffc53a8b 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -283,7 +283,7 @@ def ramp_fit_data( # If the LIKELY is selected, log that the "OLS" algorithm is being use # and note the minimum number of ngroups needed. if algorithm.upper() == "LIKELY" and ngroups < likely_min_ngroups: - msg = f"The 'OLS' algorithm is used since the LIKELY algorithm requires {likely_min_ngroups} or more" + msg = f"The 'OLS' algorithm is used since the LIKELY algorithm requires {likely_min_ngroups} or more " msg += f"NGROUPS. The NGROUPS for this data,{ngroups}, is insufficient." log.warning(msg) nframes = ramp_data.nframes From 7b22dcfc5f5a8dda125b4736dfe443e940413d11 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 5 Sep 2024 08:49:47 -0400 Subject: [PATCH 52/63] Update due to code review. --- tests/test_ramp_fitting_likely_fit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ramp_fitting_likely_fit.py b/tests/test_ramp_fitting_likely_fit.py index 7bd1fb00..e4b30091 100644 --- a/tests/test_ramp_fitting_likely_fit.py +++ b/tests/test_ramp_fitting_likely_fit.py @@ -69,7 +69,7 @@ def setup_inputs(dims, gain, rnoise, group_time, frame_time): dark_current = np.zeros(shape=(nrows, ncols), dtype=np.float32) - # Set clas arrays + # Set class arrays ramp_class.set_arrays(data, err, groupdq, pixeldq, average_dark_current=dark_current) # Set class meta From cfc0e698f1363acf8736b53749beb2ebdd99ba38 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 6 Sep 2024 09:06:16 -0400 Subject: [PATCH 53/63] Updating due to code review. --- src/stcal/ramp_fitting/likely_fit.py | 11 +++-------- src/stcal/ramp_fitting/ramp_fit.py | 10 +--------- src/stcal/ramp_fitting/ramp_fit_class.py | 2 +- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 4fa4cba7..2dfead43 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -32,11 +32,6 @@ def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): """ Invoke ramp fitting using the likelihood algorithm. - Setup the inputs to ols_ramp_fit with and without multiprocessing. The - inputs will be sliced into the number of cores that are being used for - multiprocessing. Because the data models cannot be pickled, only numpy - arrays are passed and returned as parameters to ols_ramp_fit. - Parameters ---------- ramp_data : RampData @@ -85,9 +80,9 @@ def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): for row in range(nrows): d2use = determine_diffs2use(ramp_data, integ, row, diff) d2use_copy = d2use.copy() # Use to flag jumps - if ramp_data.nsig is not None: - threshold_one_omit = ramp_data.nsig**2 - pval = scipy.special.erfc(ramp_data.nsig/SQRT2) + if ramp_data.rejection_threshold is not None: + threshold_one_omit = ramp_data.rejection_threshold**2 + pval = scipy.special.erfc(ramp_data.rejection_threshold/SQRT2) threshold_two_omit = scipy.stats.chi2.isf(pval, 2) if np.isinf(threshold_two_omit): threshold_two_omit = threshold_one_omit + 10 diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index ffc53a8b..5a6ac2c9 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -20,7 +20,7 @@ from . import ( gls_fit, # used only if algorithm is "GLS" - likely_fit, # used only if algorithm is "LIKLEY" + likely_fit, # used only if algorithm is "LIKELY" ols_fit, # used only if algorithm is "OLS" ramp_fit_class, ) @@ -97,8 +97,6 @@ def create_ramp_fit_class(model, algorithm, dqflags=None, suppress_one_group=Fal if hasattr(model.meta.exposure, "read_pattern"): ramp_data.read_pattern = [list(reads) for reads in model.meta.exposure.read_pattern] - # XXX If LIKELY, then make sure `nsig` gets set - ramp_data.set_dqflags(dqflags) ramp_data.start_row = 0 ramp_data.num_rows = ramp_data.data.shape[2] @@ -280,12 +278,6 @@ def ramp_fit_data( # Default to OLS. # Get readnoise array for calculation of variance of noiseless ramps, and # gain array in case optimal weighting is to be done - # If the LIKELY is selected, log that the "OLS" algorithm is being use - # and note the minimum number of ngroups needed. - if algorithm.upper() == "LIKELY" and ngroups < likely_min_ngroups: - msg = f"The 'OLS' algorithm is used since the LIKELY algorithm requires {likely_min_ngroups} or more " - msg += f"NGROUPS. The NGROUPS for this data,{ngroups}, is insufficient." - log.warning(msg) nframes = ramp_data.nframes readnoise_2d *= gain_2d / np.sqrt(2.0 * nframes) diff --git a/src/stcal/ramp_fitting/ramp_fit_class.py b/src/stcal/ramp_fitting/ramp_fit_class.py index fb72b229..d48e5a86 100644 --- a/src/stcal/ramp_fitting/ramp_fit_class.py +++ b/src/stcal/ramp_fitting/ramp_fit_class.py @@ -17,7 +17,7 @@ def __init__(self): # Meta information self.instrument_name = None self.read_pattern = None - self.nsig = None + self.rejection_threshold = None self.frame_time = None self.group_time = None From d26073001ee459773a19fa97d3da04decc6f6991 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Tue, 10 Sep 2024 14:45:58 -0400 Subject: [PATCH 54/63] Testing short ramp that should be switched to OLS_C. --- src/stcal/ramp_fitting/likely_fit.py | 5 +- src/stcal/ramp_fitting/ramp_fit.py | 16 +++-- tests/test_ramp_fitting_likely_fit.py | 100 ++++++++------------------ 3 files changed, 43 insertions(+), 78 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2dfead43..362f2087 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -18,12 +18,11 @@ DELIM = "=" * 80 SQRT2 = 1.41421356 +LIKELY_MIN_NGROUPS = 4 log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -BUFSIZE = 1024 * 300000 # 300Mb cache size for data section - log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) @@ -58,7 +57,7 @@ def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): nints, ngroups, nrows, ncols = ramp_data.data.shape - if ngroups < 4: + if ngroups < LIKELY_MIN_NGROUPS: raise ValueError("Likelihood fit requires at least 4 groups.") readtimes = get_readtimes(ramp_data) diff --git a/src/stcal/ramp_fitting/ramp_fit.py b/src/stcal/ramp_fitting/ramp_fit.py index 5a6ac2c9..b5981c07 100755 --- a/src/stcal/ramp_fitting/ramp_fit.py +++ b/src/stcal/ramp_fitting/ramp_fit.py @@ -192,9 +192,6 @@ def ramp_fit( # data models. ramp_data = create_ramp_fit_class(model, algorithm, dqflags, suppress_one_group) - if algorithm.upper() == "OLS_C": - ramp_data.run_c_code = True - return ramp_fit_data( ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, algorithm, weighting, max_cores, dqflags ) @@ -263,13 +260,22 @@ def ramp_fit_data( # For the LIKELY algorithm, due to the jump detection portion of the code # a minimum of a four group ramp is needed. ngroups = ramp_data.data.shape[1] - likely_min_ngroups = 4 + if algorithm.upper() == "LIKELY" and ngroups < likely_fit.LIKELY_MIN_NGROUPS: + log.info(f"When selecting the LIKELY ramp fitting algorithm the" + " ngroups needs to be a minimum of {likely_fit.LIKELY_MIN_NGROUPS}," + " but ngroups = {ngroups}. Due to this, the ramp fitting algorithm" + " is being changed to OLS_C") + algorithm = "OLS_C" + + if algorithm.upper() == "OLS_C": + ramp_data.run_c_code = True + if algorithm.upper() == "GLS": image_info, integ_info, gls_opt_info = gls_fit.gls_ramp_fit( ramp_data, buffsize, save_opt, readnoise_2d, gain_2d, max_cores ) opt_info = None - elif algorithm.upper() == "LIKELY" and ngroups >= likely_min_ngroups: + elif algorithm.upper() == "LIKELY" and ngroups >= likely_fit.LIKELY_MIN_NGROUPS: image_info, integ_info, opt_info = likely_fit.likely_ramp_fit( ramp_data, readnoise_2d, gain_2d ) diff --git a/tests/test_ramp_fitting_likely_fit.py b/tests/test_ramp_fitting_likely_fit.py index e4b30091..09ac883b 100644 --- a/tests/test_ramp_fitting_likely_fit.py +++ b/tests/test_ramp_fitting_likely_fit.py @@ -25,73 +25,6 @@ DELIM = "-" * 70 -def setup_inputs(dims, gain, rnoise, group_time, frame_time): - """ - Creates test data for testing. All ramp data is zero. - - Parameters - ---------- - dims: tuple - Four dimensions (nints, ngroups, nrows, ncols) - - gain: float - Gain noise - - rnoise: float - Read noise - - group_time: float - Group time - - frame_time: float - Frame time - - Return - ------ - ramp_class: RampClass - A RampClass with all zero data. - - gain: ndarray - A 2-D array for gain noise for each pixel. - - rnoise: ndarray - A 2-D array for read noise for each pixel. - """ - nints, ngroups, nrows, ncols = dims - - ramp_class = ramp_fit_class.RampData() # Create class - - # Create zero arrays according to dimensions - data = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) - err = np.ones(shape=(nints, ngroups, nrows, ncols), dtype=np.float32) - groupdq = np.zeros(shape=(nints, ngroups, nrows, ncols), dtype=np.uint8) - pixeldq = np.zeros(shape=(nrows, ncols), dtype=np.uint32) - dark_current = np.zeros(shape=(nrows, ncols), dtype=np.float32) - - - # Set class arrays - ramp_class.set_arrays(data, err, groupdq, pixeldq, average_dark_current=dark_current) - - # Set class meta - ramp_class.set_meta( - name="MIRI", - frame_time=frame_time, - group_time=group_time, - groupgap=0, - nframes=1, - drop_frames1=0, - ) - - # Set class data quality flags - ramp_class.set_dqflags(test_dq_flags) - - # Set noise arrays - gain = np.ones(shape=(nrows, ncols), dtype=np.float64) * gain - rnoise = np.full((nrows, ncols), rnoise, dtype=np.float32) - - return ramp_class, gain, rnoise - - def create_blank_ramp_data(dims, var, tm): """ Create empty RampData classes, as well as gain and read noise arrays, @@ -120,8 +53,8 @@ def create_blank_ramp_data(dims, var, tm): ) ramp_data.set_dqflags(test_dq_flags) - gain = np.ones(shape=(nrows, ncols), dtype=np.float64) * gval - rnoise = np.ones(shape=(nrows, ncols), dtype=np.float64) * rnval + gain = np.ones(shape=(nrows, ncols), dtype=np.float32) * gval + rnoise = np.ones(shape=(nrows, ncols), dtype=np.float32) * rnval return ramp_data, gain, rnoise @@ -494,7 +427,7 @@ def test_short_group_ramp(nframes): data1 = cube1[0][0, 0, 0] diff = abs(data - data1) assert diff < tol - dbg_print_slope_slope1(slopes, slopes1, (0, 0)) + # dbg_print_slope_slope1(slopes, slopes1, (0, 0)) def data_small_good_groups(): @@ -603,6 +536,33 @@ def test_jump_detect(): assert dq[1, 1] == JMP +def test_too_few_groups(caplog): + """ + Ensure + """ + nints, ngroups, nrows, ncols = 1, 3, 1, 1 + rnval, gval = 10.0, 5.0 + frame_time, nframes, groupgap = 10.736, 4, 1 + + dims = nints, ngroups, nrows, ncols + var = rnval, gval + tm = frame_time, nframes, groupgap + + ramp_data, gain2d, rnoise2d = create_blank_ramp_data(dims, var, tm) + + # Create a simple linear ramp. + ramp = np.array(list(range(ngroups))) * 20 + 10 + ramp_data.data[0, :, 0, 0] = ramp + + save_opt, algo, ncores = False, "LIKELY", "none" + slopes, cube, ols_opt, gls_opt = ramp_fit_data( + ramp_data, 512, save_opt, rnoise2d, gain2d, algo, "optimal", ncores, test_dq_flags + ) + + expected_log = "ramp fitting algorithm is being changed to OLS_C" + assert expected_log in caplog.text + + # ----------------------------------------------------------------- # DEBUG # ----------------------------------------------------------------- From cd90b7f38b77ebb7a1838556ae80753f657a68a1 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 11 Sep 2024 07:50:07 -0400 Subject: [PATCH 55/63] Updating code based on code review. --- src/stcal/ramp_fitting/likely_algo_classes.py | 3 +- src/stcal/ramp_fitting/likely_fit.py | 57 +++++-------------- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_algo_classes.py b/src/stcal/ramp_fitting/likely_algo_classes.py index 261b4385..fc71fd9f 100644 --- a/src/stcal/ramp_fitting/likely_algo_classes.py +++ b/src/stcal/ramp_fitting/likely_algo_classes.py @@ -250,8 +250,7 @@ def _compute_alphas_and_betas(self, mean_t, tau, N, delta_t): def calc_bias(self, countrates, sig, cvec, da=1e-7): """ - Calculate the bias in the best-fit count rate from estimating the - covariance matrix. + Calculate the bias in the best-fit count rate from estimating the covariance matrix. Section 5 of paper 1. diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 362f2087..4c44f580 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -17,7 +17,7 @@ DELIM = "=" * 80 -SQRT2 = 1.41421356 +SQRT2 = np.sqrt(2) LIKELY_MIN_NGROUPS = 4 log = logging.getLogger(__name__) @@ -137,8 +137,7 @@ def mask_jumps( ): """ - Function mask_jumps implements a likelihood-based, iterative jump - detection algorithm. + Implements a likelihood-based, iterative jump detection algorithm. Parameters ---------- @@ -163,13 +162,13 @@ def mask_jumps( Minimum chisq improvement to exclude two sequential resultant differences. Default 23.8. - d2use : ndarray + diffs2use : ndarray A boolean array definined the segmented ramps for each pixel in a row. (ngroups-1, ncols) Returns ------- - d2use : ndarray + dffs2use : ndarray A boolean array definined the segmented ramps for each pixel in a row. (ngroups-1, ncols) @@ -179,7 +178,8 @@ def mask_jumps( """ # Force a copy of the input array for more efficient memory access. - loc_diff = diffs * 1 + # loc_diff = diffs * 1 + loc_diff = diffs # We can use one-omit searches only where the reads immediately # preceding and following have just one read. If a readout @@ -452,15 +452,12 @@ def determine_diffs2use(ramp_data, integ, row, diffs): return d2use -def inital_count_rate_guess(covar, diffs, diffs2use): +def initial_count_rate_guess(diffs, diffs2use): """ Compute the initial count rate. Parameters ---------- - covar : Covar - The class instance that computes and contains the covariance matrix info. - diffs : ndarray The group differences of the data (ngroups-1, nrows, ncols). @@ -547,7 +544,7 @@ def fit_ramps( # diffs is (ngroups, ncols) of the current row if count_rate_guess is None: - count_rate_guess = inital_count_rate_guess(covar, diffs, diffs2use) + count_rate_guess = initial_count_rate_guess(covar, diffs, diffs2use) alpha_tuple, beta_tuple, scale = compute_alphas_betas( count_rate_guess, gain, rnoise, covar, rescale, diffs, dn_scale @@ -593,14 +590,10 @@ def fit_ramps( result = get_ramp_result( dC, - dB, A, B, C, scale, - phi, - theta, - covar, alpha_phnoise, alpha_readnoise, beta_phnoise, @@ -610,8 +603,11 @@ def fit_ramps( # --- Beginning at line 250: Paper 1 section 4 if detect_jumps: + # result = compute_jump_detects( + # result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar + # ) result = compute_jump_detects( - result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar + result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta ) return result @@ -621,7 +617,7 @@ def fit_ramps( def compute_jump_detects( - result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta, covar + result, ndiffs, diffs2use, dC, dB, A, B, C, scale, beta, phi, theta ): """ Detect jumps in ramps. @@ -1155,19 +1151,8 @@ def matrix_computations( def get_ramp_result( - dC, - dB, - A, - B, - C, - scale, - phi, - theta, - covar, - alpha_phnoise, - alpha_readnoise, - beta_phnoise, - beta_readnoise, + dC, A, B, C, scale, alpha_phnoise, alpha_readnoise, + beta_phnoise, beta_readnoise, ): """ Use intermediate computations to fit the ramp and save the results. @@ -1177,9 +1162,6 @@ def get_ramp_result( dC : ndarray Intermediate computation. - dB : ndarray - Intermediate computation. - A : ndarray Intermediate computation. @@ -1194,15 +1176,6 @@ def get_ramp_result( normalize its determinant in order to avoid possible overflow/underflow problems for long ramps. - phi : ndarray - Intermediate computation. - - theta : ndarray - Intermediate computation. - - covar : Covar - The class instance that computes and contains the covariance matrix info. - alpha_phnoise : ndarray The photon noise contribution to the alphas. From 6a2abb0ad278bbfa043d202d77e8eb0f7125d120 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 11 Sep 2024 07:50:55 -0400 Subject: [PATCH 56/63] Updating documentation based on code review. --- docs/stcal/ramp_fitting/description.rst | 39 +++++++++++++------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index 95141220..28ce2391 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -19,9 +19,9 @@ using the array-based functionality of numpy. The size of the block depends on the image size and the number of groups. There is a likelihood algorithm implemented based on Timothy Brandt's papers: -Optimal Fitting and Debiasing for Detectors Read Out Up-the-Ramp -Likelihood-Based Jump Detection and Cosmic Ray Rejection for Detectors Read Out Up-the-Ramp. -This algorithm is currently in beta phase as an alternative to the OLS method. +`Brandt (2024) `__ +and +`Brandt (2024) `__. .. _ramp_output_products: @@ -324,14 +324,15 @@ product. Likelihood Algorithm Details ---------------------------- As an alternative to the OLS algorithm, a likelihood algorithm can be selected -with the step argument ``--ramp_fitting.algorithm=LIKELY``. If this algorithm -is selected, the normal pipeline jump detection algorithm is skipped because -this algorithm has its own jump detection algorithm. The jump detection for -this algorithm requires NGROUPS to be a minimum of four (4). If NGROUPS -:math:`\le` 3, then this algorithm is deselected, defaulting to the above -described OLS algorithm and the normal jump detection pipeline step is run. - -Each pixel is independently processed, but rather than operate on the each +with the step argument ``--ramp_fitting.algorithm=LIKELY``. This algorithm has +its own algorithm for jump detection. The normal jump detection algorithm runs +by default, but can be skipped when selecting the LIKELY algorithm. This +algorithm removes jump detection flags and sets jump detection flags. This jump +detection algorithm requires a minimum of four (4) NGROUPS. If the LIKELY +algorithm is selected for data with NGROUPS less than four, the ramp fitting +algorithm is changed to OLS_C. + +Each pixel is independently processed, but rather than operate on each group/resultant directly, the likelihood algorithm is based on differences of the groups/resultants :math:`d_i = r_i - r_{i-1}`. The model used to determine the slope/countrate, :math:`a`, is: @@ -346,14 +347,14 @@ Differentiating, setting to zero, then solving for :math:`a` results in The covariance matrix :math:`C` is a tridiagonal matrix, due to the nature of the differences. Because the covariance matrix is tridiagonal, the computational -complexity reduces from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation -and computations implemented, refer to -`Brandt (2024) `_ and -`Brandt (2024) `__. -The Poisson and read noise computations are based on equations (27) and (28), defining -:math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the off diagonal. +complexity reduces from :math:`O(n^3)` to :math:`O(n)`. To see the detailed +derivation and computations implemented, refer to the links above. +The Poisson and read noise computations are based on equations (27) and (28), +defining :math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the +off diagonal. This algorithm runs ramp fitting twice. The first run allows for a first -approximation for the slope, hence :math:`C`, as well as to take care for any jumps -in a ramp. Using this first approximation, ramp fitting is run again without jump +approximation for the slope. This first approximation is used to create the +covariance matrix :math:`C`, as well as to take care for any jumps in a ramp. +Using this first approximation, ramp fitting is run again without jump detection to compute the final slope and variances for each pixel. From 9c7e438947a5044455f38f446526f10e8345bc4b Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 12 Sep 2024 03:38:36 -0400 Subject: [PATCH 57/63] Updating docs for likelihood algorithm in ramp fitting. --- docs/stcal/ramp_fitting/description.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index 28ce2391..e2251f6d 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -352,9 +352,3 @@ derivation and computations implemented, refer to the links above. The Poisson and read noise computations are based on equations (27) and (28), defining :math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the off diagonal. - -This algorithm runs ramp fitting twice. The first run allows for a first -approximation for the slope. This first approximation is used to create the -covariance matrix :math:`C`, as well as to take care for any jumps in a ramp. -Using this first approximation, ramp fitting is run again without jump -detection to compute the final slope and variances for each pixel. From f3ba0eb4aa7acc7d9ecf2d58cb2bbd12ae6b0a94 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Fri, 13 Sep 2024 16:31:25 -0400 Subject: [PATCH 58/63] Suppressing warnings in likelihood fit computations. --- src/stcal/ramp_fitting/likely_fit.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 4c44f580..7d345020 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -281,11 +281,13 @@ def mask_jumps( break # Store the updated counts with omitted reads - new_cts = np.zeros(np.sum(recheck)) - i_d1 = np.sum(dropone, axis=0) > 0 - new_cts[i_d1] = np.sum(result.countrate_one_omit * dropone, axis=0)[i_d1] - i_d2 = np.sum(droptwo, axis=0) > 0 - new_cts[i_d2] = np.sum(result.countrate_two_omit * droptwo, axis=0)[i_d2] + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + new_cts = np.zeros(np.sum(recheck)) + i_d1 = np.sum(dropone, axis=0) > 0 + new_cts[i_d1] = np.sum(result.countrate_one_omit * dropone, axis=0)[i_d1] + i_d2 = np.sum(droptwo, axis=0) > 0 + new_cts[i_d2] = np.sum(result.countrate_two_omit * droptwo, axis=0)[i_d2] # zero out count rates with drops and add their new values back in countrate[recheck] *= drop == 0 From f97d568c4fa5bcb9bebe4b058e231e19557fc068 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 18 Sep 2024 09:10:50 -0400 Subject: [PATCH 59/63] Updating the jump handling for the likelihood ramp fitting algorithm. --- src/stcal/ramp_fitting/likely_fit.py | 48 +++++++++++++--------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 7d345020..a4f3f9ff 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -60,6 +60,9 @@ def likely_ramp_fit(ramp_data, readnoise_2d, gain_2d): if ngroups < LIKELY_MIN_NGROUPS: raise ValueError("Likelihood fit requires at least 4 groups.") + + remove_jump_detection_flags(ramp_data) + readtimes = get_readtimes(ramp_data) covar = Covar(readtimes) @@ -393,6 +396,22 @@ def compute_image_info(integ_class, ramp_data): return (slope, dq, var_p, var_r, err) +def remove_jump_detection_flags(ramp_data): + """ + Remove the JUMP_DET flag from the group DQ array + + Parameters + ---------- + ramp_data : RampData + Input data necessary for computing ramp fitting. + """ + jump = ramp_data.flags_jump_det + gdq = ramp_data.groupdq + wh_jump = np.where(np.bitwise_and(gdq.astype(np.uint32), jump)) + gdq[wh_jump] -= jump + ramp_data.groupdq = gdq + + def determine_diffs2use(ramp_data, integ, row, diffs): """ Compute the diffs2use mask based on DQ flags of a row. @@ -418,38 +437,15 @@ def determine_diffs2use(ramp_data, integ, row, diffs): A boolean array definined the segmented ramps for each pixel in a row. (ngroups-1, ncols) """ + # import ipdb; ipdb.set_trace() _, ngroups, _, ncols = ramp_data.data.shape dq = np.zeros(shape=(ngroups, ncols), dtype=np.uint8) dq[:, :] = ramp_data.groupdq[integ, :, row, :] d2use_tmp = np.ones(shape=diffs.shape, dtype=np.uint8) d2use = d2use_tmp[:, row] - # The JUMP_DET is handled different than other group DQ flags. - jmp = np.uint8(ramp_data.flags_jump_det) - other_flags = ~jmp - - # Find all non-jump flags - oflags_locs = np.zeros(shape=dq.shape, dtype=np.uint8) - wh_of = np.where(np.bitwise_and(dq, other_flags)) - oflags_locs[wh_of] = 1 - - # Find all jump flags - jmp_locs = np.zeros(shape=dq.shape, dtype=np.uint8) - wh_j = np.where(np.bitwise_and(dq, jmp)) - jmp_locs[wh_j] = 1 - - del wh_of, wh_j - - # Based on flagging, exclude differences associated with flagged groups. - - # If a jump occurs at group k, then the difference - # group[k] - group[k-1] is excluded. - d2use[jmp_locs[1:, :] == 1] = 0 - - # If a non-jump flag occurs at group k, then the differences - # group[k+1] - group[k] and group[k] - group[k-1] are excluded. - d2use[oflags_locs[1:, :] == 1] = 0 - d2use[oflags_locs[:-1, :] == 1] = 0 + d2use[dq[1:, :] != 0] = 0 + d2use[dq[:-1, :] != 0] = 0 return d2use From fe59fd698feeb6990d25584cb85ae7b15f90c203 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 18 Sep 2024 14:06:48 -0400 Subject: [PATCH 60/63] Updating the calculations collapsing the rateints product to the rate product to account for NaN's. --- src/stcal/ramp_fitting/likely_fit.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index a4f3f9ff..2edda459 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -380,17 +380,28 @@ def compute_image_info(integ_class, ramp_data): dq = utils.dq_compress_final(integ_class.dq, ramp_data) - slope = np.median(integ_class.data, axis=0) + slope = np.nanmedian(integ_class.data, axis=0) for _ in range(2): rate_scale = slope[np.newaxis, :] / integ_class.data rate_scale[(~np.isfinite(rate_scale)) | (rate_scale < 0)] = 0 all_var_p = integ_class.var_poisson * rate_scale weight = 1/(all_var_p + integ_class.var_rnoise) - weight /= np.sum(weight, axis=0)[np.newaxis, :] - slope = np.sum(integ_class.data*weight, axis=0) + weight /= np.nansum(weight, axis=0)[np.newaxis, :] + tmp_slope = integ_class.data * weight + all_nan = np.all(np.isnan(tmp_slope), axis=0) + slope = np.sum(tmp_slope, axis=0) + slope[all_nan] = np.nan + + tmp_v = all_var_p * weight**2 + all_nan = np.all(np.isnan(tmp_v), axis=0) + var_p = np.sum(tmp_v, axis=0) + var_p[all_nan] = np.nan + + tmp_v = integ_class.var_rnoise * weight**2 + all_nan = np.all(np.isnan(tmp_v), axis=0) + var_r = np.sum(tmp_v, axis=0) + var_r[all_nan] = np.nan - var_p = np.sum(all_var_p * weight**2, axis=0) - var_r = np.sum(integ_class.var_rnoise * weight**2, axis=0) err = np.sqrt(var_p + var_r) return (slope, dq, var_p, var_r, err) From d900a8096a7b2110fc4bdc4190b0d820a05e5a81 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Wed, 18 Sep 2024 14:56:07 -0400 Subject: [PATCH 61/63] Updating the likelihood algorithm description documentation. --- docs/stcal/ramp_fitting/description.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index e2251f6d..ea635b1b 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -18,10 +18,9 @@ saturation flags are found. Pixels are processed simultaneously in blocks using the array-based functionality of numpy. The size of the block depends on the image size and the number of groups. -There is a likelihood algorithm implemented based on Timothy Brandt's papers: -`Brandt (2024) `__ -and -`Brandt (2024) `__. + +There is also a likelihood algorithm implementing an algorithm based on the group +differences of a ramp. See the detailed description below. .. _ramp_output_products: @@ -350,5 +349,10 @@ differences. Because the covariance matrix is tridiagonal, the computational complexity reduces from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation and computations implemented, refer to the links above. The Poisson and read noise computations are based on equations (27) and (28), -defining :math:`\alpha_i`, the diagonal of :math:`C`, and :math:`\beta_i`, the -off diagonal. +in the first link below, defining :math:`\alpha_i`, the diagonal of :math:`C`, +and :math:`\beta_i`, the off diagonal. + +The full details of the algorithm can be found here: +`Brandt (2024) `__ +and +`Brandt (2024) `__. From 7a070432b1d8dfb6a987b4c26e28f45bf3063c09 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 19 Sep 2024 13:00:58 -0400 Subject: [PATCH 62/63] Changing sum to nansum. --- src/stcal/ramp_fitting/likely_fit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stcal/ramp_fitting/likely_fit.py b/src/stcal/ramp_fitting/likely_fit.py index 2edda459..7a17fa97 100644 --- a/src/stcal/ramp_fitting/likely_fit.py +++ b/src/stcal/ramp_fitting/likely_fit.py @@ -389,17 +389,17 @@ def compute_image_info(integ_class, ramp_data): weight /= np.nansum(weight, axis=0)[np.newaxis, :] tmp_slope = integ_class.data * weight all_nan = np.all(np.isnan(tmp_slope), axis=0) - slope = np.sum(tmp_slope, axis=0) + slope = np.nansum(tmp_slope, axis=0) slope[all_nan] = np.nan tmp_v = all_var_p * weight**2 all_nan = np.all(np.isnan(tmp_v), axis=0) - var_p = np.sum(tmp_v, axis=0) + var_p = np.nansum(tmp_v, axis=0) var_p[all_nan] = np.nan tmp_v = integ_class.var_rnoise * weight**2 all_nan = np.all(np.isnan(tmp_v), axis=0) - var_r = np.sum(tmp_v, axis=0) + var_r = np.nansum(tmp_v, axis=0) var_r[all_nan] = np.nan err = np.sqrt(var_p + var_r) From 375f310e2d81ec0c006d5858e181b1f8b7029345 Mon Sep 17 00:00:00 2001 From: Ken MacDonald Date: Thu, 19 Sep 2024 13:49:17 -0400 Subject: [PATCH 63/63] Updating the documentation for ramp fitting wrt the likelihood algorithm. --- docs/stcal/ramp_fitting/description.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/stcal/ramp_fitting/description.rst b/docs/stcal/ramp_fitting/description.rst index ea635b1b..49f0dd0b 100644 --- a/docs/stcal/ramp_fitting/description.rst +++ b/docs/stcal/ramp_fitting/description.rst @@ -19,8 +19,9 @@ using the array-based functionality of numpy. The size of the block depends on the image size and the number of groups. -There is also a likelihood algorithm implementing an algorithm based on the group -differences of a ramp. See the detailed description below. +There is also a new algorithm available for testing, the likelihood +algorithm, implementing an algorithm based on the group differences +of a ramp. See :ref:`likelihood algorithm `. .. _ramp_output_products: @@ -320,6 +321,8 @@ that pixel will be flagged as JUMP_DET in the corresponding integration in the "rateints" product. That pixel will also be flagged as JUMP_DET in the "rate" product. +.. _likelihood_algo: + Likelihood Algorithm Details ---------------------------- As an alternative to the OLS algorithm, a likelihood algorithm can be selected @@ -348,11 +351,9 @@ The covariance matrix :math:`C` is a tridiagonal matrix, due to the nature of th differences. Because the covariance matrix is tridiagonal, the computational complexity reduces from :math:`O(n^3)` to :math:`O(n)`. To see the detailed derivation and computations implemented, refer to the links above. -The Poisson and read noise computations are based on equations (27) and (28), -in the first link below, defining :math:`\alpha_i`, the diagonal of :math:`C`, -and :math:`\beta_i`, the off diagonal. - -The full details of the algorithm can be found here: +The Poisson and read noise computations are based on equations (27) and (28), in `Brandt (2024) `__ -and + +For more details, especially for the jump detection portion in the liklihood +algorithm, see `Brandt (2024) `__.