CTI Calibration#

In the previous overview, we learnt how to fit a CTI model to a dataset and quantify its goodness-of-fit via a log likelihood.

We are now in a position to perform CTI calibration, that is determine the best-fit CTI model to a charge injection dataset. This requires us to perform model-fitting, whereby we use a non-linear search algorithm to fit the model to the data.

CTI modeling uses the probabilistic programming language PyAutoFit, an open-source Python framework that allows complex model fitting techniques to be straightforwardly integrated into scientific modeling software. Check it out if you are interested in developing your own software to perform advanced model-fitting!

Whereas previous tutorials loaded a single charge injection dataset, this tutorial will load and fit three datasets each with a different injection normalizations. This is necessary for us to be able to calibrate the CTI model’s CCD volume filling.

Dataset#

We set up the variables required to load the charge injection imaging, using the same code shown in the previous overview.

Note that the Region2D and Layout2DCI inputs have been updated to reflect the 30 x 30 shape of the dataset.

shape_native = (30, 30)

parallel_overscan = ac.Region2D((25, 30, 1, 29))
serial_prescan = ac.Region2D((0, 30, 0, 1))
serial_overscan = ac.Region2D((0, 25, 29, 30))

regions_list = [(0, 25, serial_prescan[3], serial_overscan[2])]

normalization_list = [100, 1000.0, 10000.0]

total_datasets = len(normalization_list)

layout_list = [
    ac.Layout2DCI(
        shape_2d=shape_native,
        region_list=regions_list,
        parallel_overscan=parallel_overscan,
        serial_prescan=serial_prescan,
        serial_overscan=serial_overscan,
    )
    for i in range(total_datasets)
]

We load each charge injection image, with injections of 100e-, 1000e- and 10000e- so that we have the information required to calibrate the volume filling behaviour of the CCD.

dataset_label = "overview"
dataset_type = "calibrate"
dataset_path = path.join("dataset", "imaging_ci", dataset_label, dataset_type)

imaging_ci_list = [
    ac.ImagingCI.from_fits(
        data_path=path.join(dataset_path, f"data_{int(normalization)}.fits"),
        noise_map_path=path.join(dataset_path, f"noise_map_{int(normalization)}.fits"),
        pre_cti_data_path=path.join(
            dataset_path, f"pre_cti_data_{int(normalization)}.fits"
        ),
        layout=layout,
        pixel_scales=0.1,
    )
    for layout, normalization in zip(layout_list, normalization_list)
]

Clocking#

We define the Clocker which models the CCD read-out, including CTI.

For parallel clocking, we use ‘charge injection mode’ which transfers the charge of every pixel over the full CCD.

clocker = ac.Clocker2D(parallel_express=2, parallel_roe=ac.ROEChargeInjection())

Model#

We compose the CTI model that we fit to the data using autofit Model objects.

These behave analogously to the TrapInstantCapture and CCDPhase objects but their parameters (e.g. density, well_fill_power) are not specified and are instead determined by a fitting procedure.

In this example we fit a CTI model with:

  • One parallel TrapInstantCapture’s which capture electrons during clocking instantly in the parallel direction [2 parameters].

  • A simple CCD volume filling parametrization with fixed notch depth and capacity [1 parameter].

The number of free parameters and therefore the dimensionality of non-linear parameter space is N=3.

parallel_trap_0 = af.Model(ac.TrapInstantCapture)
parallel_trap_0.density = af.UniformPrior(lower_limit=0.0, upper_limit=20.0)
parallel_trap_0.release_timescale = af.UniformPrior(lower_limit=0.0, upper_limit=20.0)

parallel_traps = [parallel_trap_0]

parallel_ccd = af.Model(ac.CCDPhase)
parallel_ccd.well_notch_depth = 0.0
parallel_ccd.full_well_depth = 200000.0

We combine the trap and CCD models above into a CTI2D and Collection object, which is the model we will fit.

The CTI2D object can be easily extended to contain model components for serial CTI. Furthermore, the Collection object can be extended to contain other components of a model other than just the CTI model, for example nuisance parameters that represent features in the CCD.

model = af.Collection(
    cti=af.Model(ac.CTI2D, parallel_trap_list=[parallel_trap_0], parallel_ccd=parallel_ccd)
)

Printing the info attribute of the model shows us this is the model we are fitting, and shows us the free parameters and their priors:

print(model.info)

This gives the following output:

cti
    trap_list
        0
            density                              UniformPrior, lower_limit = 0.0, upper_limit = 10.0
            release_timescale                    UniformPrior, lower_limit = 0.0, upper_limit = 50.0
        1
            density                              UniformPrior, lower_limit = 0.0, upper_limit = 10.0
            release_timescale                    UniformPrior, lower_limit = 0.0, upper_limit = 50.0
    ccd
        full_well_depth                          200000.0
        well_notch_depth                         0.0
        well_fill_power                          UniformPrior, lower_limit = 0.0, upper_limit = 1.0

Analysis#

analysis_list = [
    ac.AnalysisImagingCI(dataset_ci=imaging_ci, clocker=clocker_2d)
    for dataset in imaging_ci_list
]

By summing this list of analysis objects, we create an overall Analysis which we can use to fit the CTI model, where:

  • The log likelihood function of this summed analysis class is the sum of the log likelihood functions of each individual analysis object.

  • The summing process ensures that tasks such as outputting results to hard-disk, visualization, etc use a structure that separates each analysis and therefore each dataset.

analysis = sum(analysis_list)

We can parallelize the likelihood function of these analysis classes, whereby each evaluation is performed on a different CPU.

analysis.n_cores = 1

Model-Fit#

We can now begin the model-fit by passing the model and analysis object to the search, which performs a non-linear search to find which models fit the data with the highest likelihood.

All results are written to hard disk, including on-the-fly results and visualization of the best fit model!

result_list = search.fit(model=model, analysis=analysis)

Result#

The search returns a result object, which includes the charge injection fit corresponding to the maximum log likelihood solution in parameter space for every charge injection dataset.

The info attribute can be printed to give the results in a readable format:

print(result_list.info)

This gives the following output:

Bayesian Evidence                                4125.33026253
Maximum Log Likelihood                           4165.57074867
Maximum Log Posterior                            4165.57074867

model   Collection (N=5)
    cti CTI1D (N=5)
        trap_list                                Collection (N=4)
            0                                    TrapInstantCapture (N=2)
            1                                    TrapInstantCapture (N=2)
        ccd                                      CCDPhase (N=1)

Maximum Log Likelihood Model:

cti
    trap_list
        0
            density                              0.130
            release_timescale                    1.246
        1
            density                              0.251
            release_timescale                    4.401
    ccd
        well_fill_power                          0.580


Summary (3.0 sigma limits):

cti
    trap_list
        0
            density                              0.13 (0.13, 0.13)
            release_timescale                    1.25 (1.23, 1.26)
        1
            density                              0.25 (0.25, 0.25)
            release_timescale                    4.40 (4.37, 4.43)
    ccd
        well_fill_power                          0.58 (0.58, 0.58)


Summary (1.0 sigma limits):

cti
    trap_list
        0
            density                              0.13 (0.13, 0.13)
            release_timescale                    1.25 (1.24, 1.25)
        1
            density                              0.25 (0.25, 0.25)
            release_timescale                    4.40 (4.39, 4.41)
    ccd
        well_fill_power                          0.58 (0.58, 0.58)

instances

cti
    ccd
        full_well_depth                          200000.0
        well_notch_depth                         0.0

Below, we plot a subplot of the fit for the first dataset which shows a good fit has been inferred.

fit_plotter = aplt.FitImagingCIPlotter(fit=result_list[0].max_log_likelihood_fit)
fit_plotter.subplot_fit()
Alternative text

It also contains the maximum likelihood CTI model, which allows us to print the maximum likelihood values of the inferred CTI model parameters.

Note how this object uses the same API as the Collection and Model we composed above (e.g. the model component above was named parallel_traps, which is used below).

cti_model = result_list[0].max_log_likelihood_instance.cti

print(cti_model.parallel_traps[0].density)
print(cti_model.parallel_traps[0].release_timescale)
print(cti_model.parallel_ccd.well_fill_power)

Calibration in 1D#

We can also perform CTI calibration on 1D datasets.

shape_native = (30,)

prescan = ac.Region1D((0, 1))
overscan = ac.Region1D((25, 30))

region_list = [(1, 25)]

normalization_list = [100.0, 1000.0, 10000.0]

layout_list = [
    ac.Layout1D(
        shape_1d=shape_native,
        region_list=region_list,
        prescan=prescan,
        overscan=overscan,
    )
    for normalization in normalization_list
]

dataset_list = [
    ac.Dataset1D.from_fits(
        data_path=path.join(dataset_path, f"data_{int(normalization)}.fits"),
        noise_map_path=path.join(dataset_path, f"noise_map_{int(normalization)}.fits"),
        pre_cti_data_path=path.join(
            dataset_path, f"pre_cti_data_{int(normalization)}.fits"
        ),
        layout=layout,
        pixel_scales=0.1,
    )
    for layout, normalization in zip(layout_list, normalization_list)
]

clocker_1d = ac.Clocker1D(express=2)

We define the Clocker1D, which models the CCD read-out, including CTI.

For parallel clocking, we use ‘charge injection mode’ read-out electronics object. Which transfers the charge of every pixel over the full CCD.

clocker_1d = ac.Clocker1D(express=2, roe=ac.ROEChargeInjection())

We again compose a CTI model that we fit to the data using autofit Model objects.

trap_0 = af.Model(ac.TrapInstantCapture)
trap_0.density = af.UniformPrior(lower_limit=0.0, upper_limit=20.0)
trap_0.release_timescale = af.UniformPrior(lower_limit=0.0, upper_limit=50.0)

traps = [trap_0]

ccd = af.Model(ac.CCDPhase)
ccd.well_notch_depth = 0.0
ccd.full_well_depth = 200000.0

We combine the trap and CCD models above into a CTI1D and Collection object, which is the model we will fit.

model = af.Collection(cti=af.Model(ac.CTI1D, traps=traps, ccd=ccd))

We again use dynesty (https://github.com/joshspeagle/dynesty) to fit the model.

search = af.Nautilus(name="overview_modeling_1d")

We next create a list of AnalysisDataset1D objects, which each contain a log_likelihood_function that the non-linear search calls to fit the CIT model to the data.

We again sum these analyses objects into a single analysis.

analysis_list = [
    ac.AnalysisDataset1D(dataset_1d=dataset_1d, clocker=clocker_1d)
    for dataset in dataset_list
]

analysis = sum(analysis_list)

analysis.n_cores = 1

We can now begin the model-fit by passing the model and analysis object to the search, which performs a non-linear search to find which models fit the data with the highest likelihood.

result_list = search.fit(model=model, analysis=analysis)

The search returns a result object, which again allows us to print the maximum likelihood CTI model and plot the maximum likelihood fit.

cti_model = result_list[0].max_log_likelihood_instance.cti

print(cti_model.parallel_traps[0].density)
print(cti_model.parallel_traps[0].release_timescale)
print(cti_model.parallel_ccd.well_fill_power)

fit_plotter = aplt.FitDataset1DPlotter(fit=result.max_log_likelihood_fit)
fit_plotter.subplot_fit()
Alternative text

Wrap Up#

A full overview of the CTI results is given at autocti_workspace/*/results.