In [1]:
# What version of the Stack are we using?
! echo $IMAGE_DESCRIPTION
! eups list -s | grep lsst_distrib
Release r29.2.0 (RSP Build 2244)
lsst_distrib          gc675d380bf+f75de59d28 	current o_latest v29_2_0 v29_2_0_rc1 setup
In [2]:
import numpy as np
import matplotlib.pyplot as plt
import lsst.afw.display as afwDisplay

from lsst.daf.butler import Butler

from lsst.ip.isr import isrFunctions
import lsst.afw.math as afwMath
from lsst.meas.algorithms import backgroundFlatContext

afwDisplay.setDefaultBackend("matplotlib")
In [3]:
# Always use this repo and collection in DP1
repo = 'dp1'
collections = 'LSSTComCam/DP1'

butler = Butler(repo, collections=collections)
In [4]:
dataId = {'instrument': 'LSSTComCam', 'detector': 8, 'visit': 2024113000204, 'band': 'u', 'day_obs': 20241130, 'physical_filter': 'u_02'}

Grab the configs used by ReprocessVisitImageTask and IsrTaskLSST

In [5]:
reprocessVisitImage_config = butler.get('reprocessVisitImage_config', dataId=dataId)
isr_config = butler.get('isr_config', dataId=dataId)

If this is True, an illuminationCorrection was applied to the visit_image.

In [6]:
reprocessVisitImage_config.do_apply_flat_background_ratio
Out[6]:
True
In [7]:
illuminationCorrection = butler.get('illuminationCorrection', dataId=dataId)

If these two things are False, we don't need to worry about some other potential complications.

In [8]:
reprocessVisitImage_config.remove_initial_photo_calib, reprocessVisitImage_config.do_use_sky_corr
Out[8]:
(False, False)

Grab the visit_image, and its background.

In [9]:
visit_image = butler.get('visit_image', dataId=dataId)
In [10]:
visit_image_background = butler.get('visit_image_background', dataId=dataId)

If this is "electron", then the relevant steps of ISR assume a gain of 1.0.

In [11]:
isElectrons = (visit_image.metadata["LSST ISR UNITS"] == "electron")
print('LSST ISR UNITS:', visit_image.metadata['LSST ISR UNITS'])
LSST ISR UNITS: electron

Items needed to invert the flat correction

In [12]:
maskedImage = visit_image.maskedImage

flat = butler.get('flat', dataId=dataId)
flatMaskedImage = flat.maskedImage

scalingType = isr_config.flatScalingType
userScale = isr_config.flatUserScale

Pathological pixels have known sources of disagreement between their variance and flux. We keep track of these using the mask plane.

In [13]:
maskPlaneDict = maskedImage.getMask().getMaskPlaneDict()
In [14]:
bad_mask_names = ['BAD', 'CR', 'CROSSTALK', 'INTRP', 'NO_DATA', 'SAT', 'SUSPECT', 'UNMASKEDNAN']

Illustrations of some of the things we're working with.

In [15]:
plt.imshow(np.arcsinh(maskedImage.image.array), origin='lower')
plt.show()
No description has been provided for this image
In [16]:
plt.imshow(np.arcsinh(maskedImage.variance.array), origin='lower')
plt.show()
No description has been provided for this image
In [17]:
plt.imshow(flat.image.array, origin='lower')
plt.colorbar()
plt.show()
No description has been provided for this image
In [18]:
plt.imshow(illuminationCorrection.image.array, origin='lower')
plt.colorbar()
plt.show()
No description has been provided for this image
In [19]:
plt.imshow(visit_image_background.getImage().array, origin='lower')
plt.colorbar()
plt.show()
No description has been provided for this image

Do these steps in the reverse order in which they're applied in ReprocessVisitImageTask.run(), so that we effectively work backward from a visit_image to a post_isr_image.

In [20]:
background_to_photometric_ratio = illuminationCorrection.image.clone()

# Restore the visit image background
with backgroundFlatContext(
    maskedImage,
    reprocessVisitImage_config.do_apply_flat_background_ratio,
    backgroundToPhotometricRatio=background_to_photometric_ratio,
):
    maskedImage += visit_image_background.getImage()

# Invert the illumination correction
if reprocessVisitImage_config.do_apply_flat_background_ratio:
    maskedImage *= background_to_photometric_ratio

Now look at IsrTaskLSST.run() and work backward to just after the point where the variance was computed.

In [21]:
# Invert the flat correction
isrFunctions.flatCorrection(
    maskedImage=maskedImage,
    flatMaskedImage=flatMaskedImage,
    scalingType=scalingType,
    userScale=userScale,
    invert=True
)

At this stage, the variance in each amplifier region should equal the image flux in each pixel (if everything is in electron units), plus a constant readnoise value. We can check the extent to which that's true by seeing if the variance vs. flux in each pixel obeys a straight-line relationship.

In [22]:
amps = visit_image.getDetector().getAmplifiers()
amp_bboxes = [amp.getBBox() for amp in amps]

gn = maskedImage.image.clone()
rn = maskedImage.image.clone()
gains, readnoises = [], []
fit_slopes, fit_slope_errors = [], []
fit_intercepts, fit_intercept_errors = [], []

for amp_bbox in amp_bboxes:
    amp_im_arr = maskedImage.image[amp_bbox].array
    amp_var_arr = maskedImage.variance[amp_bbox].array
    amp_mask_arr = maskedImage.mask[amp_bbox].array
    
    good = np.isfinite(amp_im_arr) & np.isfinite(amp_var_arr)
    for mask_name in bad_mask_names:
        if mask_name in maskPlaneDict:
            good &= ((amp_mask_arr & (1 << maskPlaneDict[mask_name])) == 0)
    
    fit, fit_cov = np.polyfit(amp_im_arr[good], amp_var_arr[good], deg=1, cov=True)
    gain = 1.0 / fit[0]
    readnoise = np.sqrt(gain * fit[1])
    gn[amp_bbox].array[:, :] = gain
    rn[amp_bbox].array[:, :] = readnoise

    plt.figure(figsize=(4,4))
    plt.scatter(amp_im_arr[good].flatten(), amp_var_arr[good].flatten())
    flux_range = np.array([amp_im_arr[good].min(), amp_im_arr[good].max()])
    var_range = fit[0] * flux_range + fit[1]
    plt.plot(flux_range, var_range, color='red', label='Least squares fit')
    plt.xlabel('Instrumental flux', size=16)
    plt.ylabel('Variance', size=16)
    plt.xticks(size=14)
    plt.yticks(size=14)
    plt.legend(loc='upper left')
    plt.show()

    gains.append(float(gain))
    readnoises.append(float(readnoise))
    fit_slopes.append(float(fit[0]))
    fit_slope_errors.append(float(np.sqrt(fit_cov[0][0])))
    fit_intercepts.append(float(fit[1]))
    fit_intercept_errors.append(float(np.sqrt(fit_cov[1][1])))

print('fit gains:', gains)
print('target gains:', [amp.getGain() if not isElectrons else 1.0 for amp in amps])
print('stored gains:', [amp.getGain() for amp in amps])
print('gains from metadata:', [visit_image.metadata[f"LSST ISR GAIN {amp.getName()}"] for amp in amps])
print('fit read noises (using fit gain):', readnoises)
print('stored readnoises:', [amp.getReadNoise() for amp in amps])
print('readnoises from metadata:', [visit_image.metadata[f"LSST ISR READNOISE {amp.getName()}"] for amp in amps])
print('fit_slopes:', fit_slopes)
print('fit_slope_errors:', fit_slope_errors)
print('fit_intercepts:', fit_intercepts)
print('fit_intercept_errors:', fit_intercept_errors)

plt.figure(figsize=(6,6))
plt.imshow(gn.array, origin='lower', cmap='gray')
plt.title('Effective gains [electrons/adu]')
plt.colorbar()
plt.show()

plt.figure(figsize=(6,6))
plt.imshow(rn.array, origin='lower', cmap='gray')
plt.title('Effective readnoises [electrons/pixel]')
plt.colorbar()
plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
fit gains: [0.4012335058643878, 0.39134485805814817, 0.3953049250369523, 0.4041127281093877, 0.3787299135378056, 0.33498936892425557, 0.3979216444780112, 0.39818580711378043, 0.2802852982871276, 0.4011837604644625, 0.40288499547131484, 0.38712360275036845, 0.39791402278459176, 0.38306058351035105, 0.4029936515054312, 0.40182419795152696]
target gains: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
stored gains: [1.4044, 1.3889, 1.3973, 1.4016, 1.3937, 1.3369, 1.379, 1.3528, 1.3745, 1.3918, 1.3844, 1.3792, 1.4057, 1.4183, 1.4144, 1.4273]
gains from metadata: [1.70924427200861, 1.6998016925389, 1.70168603319942, 1.70044034400547, 1.69489085722979, 1.68396249983445, 1.67826381282722, 1.64244505984655, 1.64017217346943, 1.67084618273791, 1.67510440984028, 1.67580603497722, 1.67822074394158, 1.69733321199622, 1.69503179889932, 1.70818255134929]
fit read noises (using fit gain): [21.375420247192977, 22.440284452712852, 24.003877788480377, 24.941844524491454, 23.12314781698935, 20.819873338125404, 20.696712025705587, 20.538172160326734, 16.826695947268366, 22.467914467034767, 24.29162519247011, 24.881804599304306, 25.61992887517685, 25.08800715166701, 24.042497004167245, 22.496293678827325]
stored readnoises: [8.3, 9.5, 10.0, 10.4, 10.2, 9.3, 8.0, 7.7, 8.1, 8.8, 10.0, 10.8, 11.0, 10.9, 10.1, 9.0]
readnoises from metadata: [11.3885494940233, 12.3220056878356, 13.2911127234589, 13.6725500034007, 12.9861768926762, 12.444939028196, 10.8320087246049, 10.6212321297079, 11.0022978528847, 11.9689924588829, 13.1451619997689, 13.9867322288309, 14.2260228070891, 14.22095622502, 13.0727549547783, 12.0056058375806]
fit_slopes: [2.492314289270718, 2.5552910161181024, 2.529692742650555, 2.4745570491640487, 2.640404056438964, 2.985169359885298, 2.51305756768217, 2.511390366342849, 3.5677932667577448, 2.492623327629887, 2.4820978970193477, 2.5831543023865606, 2.513105703091403, 2.610553116261773, 2.481428668328595, 2.4886505220390744]
fit_slope_errors: [0.0003252776624748071, 0.0001611959110032728, 0.00020225970872419334, 0.0006960689207502442, 0.00013312933531125753, 0.00034763155867758726, 0.00026479413956254967, 0.0005364954981752878, 0.0006053309036035208, 0.000398546395154989, 0.0006686237249611295, 0.00013730004879079113, 0.000313003519083561, 0.00016141373268683905, 0.0006666129156430662, 0.0005007322530851704]
fit_intercepts: [1138.7598096020856, 1286.7586118733252, 1457.57391924839, 1539.4110727329323, 1411.7711483937653, 1293.9727825022303, 1076.4779815807601, 1059.3459338611917, 1010.1767671444829, 1258.2941540643612, 1464.6439086199773, 1599.2416781603013, 1649.554220220201, 1643.1032842748418, 1434.3691520599566, 1259.4642927529624]
fit_intercept_errors: [0.0330403862101013, 0.029226589815756272, 0.03195464397986767, 0.059604765318242645, 0.039169319531257935, 0.3055744007986836, 0.02808445235382666, 0.04683075961816629, 1.1583113503004363, 0.037422774664628865, 0.057043326563551405, 0.03568548415483605, 0.03893618308528594, 0.03689998425808659, 0.058917122615201385, 0.04736760709804708]
No description has been provided for this image
No description has been provided for this image