diff --git a/setup.py b/setup.py index 03eedc6ff..b34e7dfa7 100644 --- a/setup.py +++ b/setup.py @@ -31,12 +31,6 @@ include_dirs=[np.get_include()], language='c++' ), - Extension( - 'stcal.ramp_fitting.ols_cas22._wrappers', - ['src/stcal/ramp_fitting/ols_cas22/_wrappers.pyx'], - include_dirs=[np.get_include()], - language='c++' - ), ] setup(ext_modules=cythonize(extensions)) diff --git a/src/stcal/ramp_fitting/ols_cas22/_fixed.pxd b/src/stcal/ramp_fitting/ols_cas22/_fixed.pxd index ed99c7de9..720870512 100644 --- a/src/stcal/ramp_fitting/ols_cas22/_fixed.pxd +++ b/src/stcal/ramp_fitting/ols_cas22/_fixed.pxd @@ -18,4 +18,4 @@ cdef class FixedValues: cdef float[:, :] var_slope_vals(FixedValues self) -cdef FixedValues fixed_values_from_metadata(ReadPatternMetadata data, Thresh threshold, bool use_jump) +cpdef FixedValues fixed_values_from_metadata(ReadPatternMetadata data, Thresh threshold, bool use_jump) diff --git a/src/stcal/ramp_fitting/ols_cas22/_fixed.pyx b/src/stcal/ramp_fitting/ols_cas22/_fixed.pyx index 6296bbaf6..6bd72b07f 100644 --- a/src/stcal/ramp_fitting/ols_cas22/_fixed.pyx +++ b/src/stcal/ramp_fitting/ols_cas22/_fixed.pyx @@ -10,8 +10,10 @@ FixedValues : class Functions --------- -fixed_values_from_metadata : function - Fast constructor for FixedValues from the read pattern metadata + fixed_values_from_metadata : function + Fast constructor for FixedValues from the read pattern metadata + - cpdef gives a python wrapper, but the python version of this method + is considered private, only to be used for testing """ import numpy as np cimport numpy as np @@ -175,8 +177,64 @@ cdef class FixedValues: return var_slope_vals - -cdef inline FixedValues fixed_values_from_metadata(ReadPatternMetadata data, Thresh threshold, bool use_jump): + def _to_dict(FixedValues self): + """ + This is a private method to convert the FixedValues object to a dictionary, + so that attributes can be directly accessed in python. Note that this + is needed because class attributes cannot be accessed on cython classes + directly in python. Instead they need to be accessed or set using a + python compatible method. This method is a pure puthon method bound + to to the cython class and should not be used by any cython code, and + only exists for testing purposes. + """ + cdef np.ndarray[float, ndim=2] t_bar_diffs + cdef np.ndarray[float, ndim=2] t_bar_diff_sqrs + cdef np.ndarray[float, ndim=2] read_recip_coeffs + cdef np.ndarray[float, ndim=2] var_slope_coeffs + + if self.use_jump: + t_bar_diffs = np.array(self.t_bar_diffs, dtype=np.float32) + t_bar_diff_sqrs = np.array(self.t_bar_diff_sqrs, dtype=np.float32) + read_recip_coeffs = np.array(self.read_recip_coeffs, dtype=np.float32) + var_slope_coeffs = np.array(self.var_slope_coeffs, dtype=np.float32) + else: + try: + self.t_bar_diffs + except AttributeError: + t_bar_diffs = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("t_bar_diffs should not exist") + + try: + self.t_bar_diff_sqrs + except AttributeError: + t_bar_diff_sqrs = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("t_bar_diff_sqrs should not exist") + + try: + self.read_recip_coeffs + except AttributeError: + read_recip_coeffs = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("read_recip_coeffs should not exist") + + try: + self.var_slope_coeffs + except AttributeError: + var_slope_coeffs = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("var_slope_coeffs should not exist") + + return dict(data=self.data, + threshold=self.threshold, + t_bar_diffs=t_bar_diffs, + t_bar_diff_sqrs=t_bar_diff_sqrs, + read_recip_coeffs=read_recip_coeffs, + var_slope_coeffs=var_slope_coeffs) + + +cpdef inline FixedValues fixed_values_from_metadata(ReadPatternMetadata data, Thresh threshold, bool use_jump): """ Fast constructor for FixedValues class Use this instead of an __init__ because it does not incure the overhead diff --git a/src/stcal/ramp_fitting/ols_cas22/_pixel.pxd b/src/stcal/ramp_fitting/ols_cas22/_pixel.pxd index 326c1d1f5..bf390419b 100644 --- a/src/stcal/ramp_fitting/ols_cas22/_pixel.pxd +++ b/src/stcal/ramp_fitting/ols_cas22/_pixel.pxd @@ -20,4 +20,4 @@ cdef class Pixel: cdef RampFits fit_ramps(Pixel self, stack[RampIndex] ramps) -cdef Pixel make_pixel(FixedValues fixed, float read_noise, float [:] resultants) +cpdef Pixel make_pixel(FixedValues fixed, float read_noise, float [:] resultants) diff --git a/src/stcal/ramp_fitting/ols_cas22/_pixel.pyx b/src/stcal/ramp_fitting/ols_cas22/_pixel.pyx index 6991811b9..88544243f 100644 --- a/src/stcal/ramp_fitting/ols_cas22/_pixel.pyx +++ b/src/stcal/ramp_fitting/ols_cas22/_pixel.pyx @@ -9,8 +9,10 @@ Pixel : class Functions --------- -make_pixel : function - Fast constructor for a Pixel class from input data. + make_pixel : function + Fast constructor for a Pixel class from input data. + - cpdef gives a python wrapper, but the python version of this method + is considered private, only to be used for testing """ from libc.math cimport sqrt, fabs from libcpp.vector cimport vector @@ -473,10 +475,50 @@ cdef class Pixel: return ramp_fits + def _to_dict(Pixel self): + """ + This is a private method to convert the Pixel object to a dictionary, so + that attributes can be directly accessed in python. Note that this is + needed because class attributes cannot be accessed on cython classes + directly in python. Instead they need to be accessed or set using a + python compatible method. This method is a pure puthon method bound + to to the cython class and should not be used by any cython code, and + only exists for testing purposes. + """ + + cdef np.ndarray[float, ndim=1] resultants_ = np.array(self.resultants, dtype=np.float32) + + cdef np.ndarray[float, ndim=2] local_slopes + cdef np.ndarray[float, ndim=2] var_read_noise + + if self.fixed.use_jump: + local_slopes = np.array(self.local_slopes, dtype=np.float32) + var_read_noise = np.array(self.var_read_noise, dtype=np.float32) + else: + try: + self.local_slopes + except AttributeError: + local_slopes = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("local_slopes should not exist") + + try: + self.var_read_noise + except AttributeError: + var_read_noise = np.array([[np.nan],[np.nan]], dtype=np.float32) + else: + raise AttributeError("var_read_noise should not exist") + + return dict(fixed=self.fixed._to_dict(), + resultants=resultants_, + read_noise=self.read_noise, + local_slopes=local_slopes, + var_read_noise=var_read_noise) + @cython.boundscheck(False) @cython.wraparound(False) -cdef inline Pixel make_pixel(FixedValues fixed, float read_noise, float [:] resultants): +cpdef inline Pixel make_pixel(FixedValues fixed, float read_noise, float [:] resultants): """ Fast constructor for the Pixel C class. This creates a Pixel object for a single pixel from the input data. diff --git a/src/stcal/ramp_fitting/ols_cas22/_wrappers.pyx b/src/stcal/ramp_fitting/ols_cas22/_wrappers.pyx deleted file mode 100644 index 53f1470c3..000000000 --- a/src/stcal/ramp_fitting/ols_cas22/_wrappers.pyx +++ /dev/null @@ -1,121 +0,0 @@ -import numpy as np -cimport numpy as np - -from libcpp cimport bool - -from stcal.ramp_fitting.ols_cas22._core cimport ReadPatternMetadata, Thresh - -from stcal.ramp_fitting.ols_cas22._fixed cimport FixedValues -from stcal.ramp_fitting.ols_cas22._fixed cimport fixed_values_from_metadata as c_fixed_values_from_metadata - -from stcal.ramp_fitting.ols_cas22._pixel cimport Pixel -from stcal.ramp_fitting.ols_cas22._pixel cimport make_pixel as c_make_pixel - - -def fixed_values_from_metadata(np.ndarray[float, ndim=1] t_bar, - np.ndarray[float, ndim=1] tau, - np.ndarray[int, ndim=1] n_reads, - float intercept, - float constant, - bool use_jump): - - cdef ReadPatternMetadata data = ReadPatternMetadata(t_bar, tau, n_reads) - cdef Thresh threshold = Thresh(intercept, constant) - - cdef FixedValues fixed = c_fixed_values_from_metadata(data, threshold, use_jump) - - cdef float intercept_ = fixed.threshold.intercept - cdef float constant_ = fixed.threshold.constant - - cdef np.ndarray[float, ndim=2] t_bar_diffs - cdef np.ndarray[float, ndim=2] t_bar_diff_sqrs - cdef np.ndarray[float, ndim=2] read_recip_coeffs - cdef np.ndarray[float, ndim=2] var_slope_coeffs - - if use_jump: - t_bar_diffs = np.array(fixed.t_bar_diffs, dtype=np.float32) - t_bar_diff_sqrs = np.array(fixed.t_bar_diff_sqrs, dtype=np.float32) - read_recip_coeffs = np.array(fixed.read_recip_coeffs, dtype=np.float32) - var_slope_coeffs = np.array(fixed.var_slope_coeffs, dtype=np.float32) - else: - try: - fixed.t_bar_diffs - except AttributeError: - t_bar_diffs = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("t_bar_diffs should not exist") - - try: - fixed.t_bar_diff_sqrs - except AttributeError: - t_bar_diff_sqrs = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("t_bar_diff_sqrs should not exist") - - try: - fixed.read_recip_coeffs - except AttributeError: - read_recip_coeffs = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("read_recip_coeffs should not exist") - - try: - fixed.var_slope_coeffs - except AttributeError: - var_slope_coeffs = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("var_slope_coeffs should not exist") - - return dict(data=fixed.data, - intercept=intercept_, - constant=constant_, - t_bar_diffs=t_bar_diffs, - t_bar_diff_sqrs=t_bar_diff_sqrs, - read_recip_coeffs=read_recip_coeffs, - var_slope_coeffs=var_slope_coeffs) - - -def make_pixel(np.ndarray[float, ndim=1] resultants, - np.ndarray[float, ndim=1] t_bar, - np.ndarray[float, ndim=1] tau, - np.ndarray[int, ndim=1] n_reads, - float read_noise, - float intercept, - float constant, - bool use_jump): - - cdef ReadPatternMetadata data = ReadPatternMetadata(t_bar, tau, n_reads) - cdef Thresh threshold = Thresh(intercept, constant) - - cdef FixedValues fixed = c_fixed_values_from_metadata(data, threshold, use_jump) - - cdef Pixel pixel = c_make_pixel(fixed, read_noise, resultants) - - cdef np.ndarray[float, ndim=1] resultants_ = np.array(pixel.resultants, dtype=np.float32) - - cdef np.ndarray[float, ndim=2] local_slopes - cdef np.ndarray[float, ndim=2] var_read_noise - - if use_jump: - local_slopes = np.array(pixel.local_slopes, dtype=np.float32) - var_read_noise = np.array(pixel.var_read_noise, dtype=np.float32) - else: - try: - pixel.local_slopes - except AttributeError: - local_slopes = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("local_slopes should not exist") - - try: - pixel.var_read_noise - except AttributeError: - var_read_noise = np.array([[np.nan],[np.nan]], dtype=np.float32) - else: - raise AttributeError("var_read_noise should not exist") - - # only return computed values (assume fixed is correct) - return dict(resultants=resultants_, - read_noise=pixel.read_noise, - local_slopes=local_slopes, - var_read_noise=var_read_noise) diff --git a/tests/test_jump_cas22.py b/tests/test_jump_cas22.py index 82e54e68b..5d49597f5 100644 --- a/tests/test_jump_cas22.py +++ b/tests/test_jump_cas22.py @@ -3,7 +3,8 @@ from numpy.testing import assert_allclose from stcal.ramp_fitting.ols_cas22._core import metadata_from_read_pattern, threshold -from stcal.ramp_fitting.ols_cas22._wrappers import fixed_values_from_metadata, make_pixel +from stcal.ramp_fitting.ols_cas22._fixed import fixed_values_from_metadata +from stcal.ramp_fitting.ols_cas22._pixel import make_pixel from stcal.ramp_fitting.ols_cas22 import fit_ramps, Parameter, Variance, Diff, RampJumpDQ @@ -126,7 +127,7 @@ def test_threshold(): intercept - constant * log10(slope) = threshold """ - # Create the python analog of the threshold struct + # Create the python analog of the Threshold struct # Note that structs get mapped to/from python as dictionary objects with # the keys being the struct members. thresh = { @@ -174,17 +175,33 @@ def test_fixed_values_from_metadata(ramp_data, use_jump): """Test computing the fixed data for all pixels""" _, t_bar, tau, n_reads = ramp_data - intercept = np.float32(5.5) - constant = np.float32(1/3) + # Create the python analog of the ReadPatternMetadata struct + # Note that structs get mapped to/from python as dictionary objects with + # the keys being the struct members. + data = { + "t_bar": t_bar, + "tau": tau, + "n_reads": n_reads, + } + + # Create the python analog of the Threshold struct + # Note that structs get mapped to/from python as dictionary objects with + # the keys being the struct members. + thresh = { + 'intercept': np.float32(5.5), + 'constant': np.float32(1/3) + } - fixed = fixed_values_from_metadata(t_bar, tau, n_reads, intercept, constant, use_jump) + # Note this is converted to a dictionary so we can directly interrogate the + # variables in question + fixed = fixed_values_from_metadata(data, thresh, use_jump)._to_dict() # Basic sanity checks that data passed in survives assert (fixed['data']['t_bar'] == t_bar).all() assert (fixed['data']['tau'] == tau).all() assert (fixed['data']['n_reads'] == n_reads).all() - assert fixed["intercept"] == intercept - assert fixed["constant"] == constant + assert fixed['threshold']["intercept"] == thresh['intercept'] + assert fixed['threshold']["constant"] == thresh['constant'] # Check the computed data # These are computed via vectorized operations in the main code, here we @@ -298,15 +315,29 @@ def test_make_pixel(pixel_data, use_jump): """Test computing the initial pixel data""" resultants, t_bar, tau, n_reads = pixel_data - intercept = np.float32(5.5) - constant = np.float32(1/3) + # Create a fixed object to pass into the constructor + # This requires setting up some structs as dictionaries + data = { + "t_bar": t_bar, + "tau": tau, + "n_reads": n_reads, + } + thresh = { + 'intercept': np.float32(5.5), + 'constant': np.float32(1/3) + } + fixed = fixed_values_from_metadata(data, thresh, use_jump) - pixel = make_pixel(resultants, t_bar, tau, n_reads, READ_NOISE, intercept, constant, use_jump) + # Note this is converted to a dictionary so we can directly interrogate the + # variables in question + pixel = make_pixel(fixed, READ_NOISE, resultants)._to_dict() # Basic sanity checks that data passed in survives assert (pixel['resultants'] == resultants).all() assert READ_NOISE == pixel['read_noise'] + # the "fixed" data is not checked as this is already done above + # Check the computed data # These are computed via vectorized operations in the main code, here we # check using item-by-item operations