Pixel Calibration

Overview

The pixel calibration component in daolite (Durham Adaptive Optics Latency Inspection and Timing Estimator) handles the preprocessing of raw camera data before wavefront sensing. This preprocessing is essential for accurate wavefront measurements and includes dark frame subtraction, flat fielding, bad pixel correction, and other operations to prepare the raw pixel data for centroiding.

Key Calibration Operations

  • Dark Subtraction: Removal of dark current and bias

  • Flat Fielding: Correction for pixel sensitivity variations

  • Bad Pixel Correction: Handling of defective pixels

  • Background Estimation: Subtraction of background flux

  • Thresholding: Application of intensity thresholds

  • Normalization: Scaling of pixel intensities

Using Calibration Components

daolite makes it easy to add pixel calibration stages to your AO pipeline:

from daolite import Pipeline, PipelineComponent, ComponentType
from daolite.pipeline.calibration import PixelCalibration
from daolite.compute import hardware
import numpy as np

pipeline = Pipeline()

# Define a CPU resource
cpu = hardware.amd_epyc_7763()

# Define pixel agenda (how many pixels per iteration)
n_pixels = 1024 * 1024
n_groups = 10
pixel_agenda = np.ones(n_groups, dtype=int) * (n_pixels // n_groups)

# Add pixel calibration component
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Pixel Calibration",
    compute=cpu,
    function=PixelCalibration,
    params={
        "pixel_agenda": pixel_agenda,
        "bit_depth": 16,  # 16-bit camera data
        "n_workers": 1
    },
    dependencies=["Camera"]
))

Calibration Configuration

The pixel calibration component accepts the following parameters:

params={
    # Required parameters
    "pixel_agenda": pixel_agenda,  # Processing agenda (np.ndarray)

    # Optional parameters
    "bit_depth": 16,       # Bit depth of pixel data (default: 16)
    "n_workers": 1,        # Number of parallel workers (default: 1)
    "flop_scale": 1.0,     # FLOP scaling factor (default: 1.0)
    "mem_scale": 1.0,      # Memory scaling factor (default: 1.0)
    "debug": False         # Enable debug output (default: False)
}

Understanding Pixel Calibration

The PixelCalibration function models the computational latency of preprocessing raw camera data before wavefront sensing. This includes operations like dark frame subtraction, flat fielding, and other pixel-level corrections.

The function uses an agenda-based API where you specify how many pixels to process in each iteration through the pixel_agenda parameter. This allows modeling of pipelined or batched processing patterns.

Key Factors Affecting Performance

The computational cost of pixel calibration depends on:

  • Number of Pixels: Scales linearly with pixel count

  • Bit Depth: Higher bit depths require more memory bandwidth

  • Memory Bandwidth: Calibration operations are typically memory-bound

  • Computational Resource: CPU vs GPU implementation affects throughput

Practical Example

Complete Example: Full Pipeline with Calibration

from daolite import Pipeline, PipelineComponent, ComponentType
from daolite.pipeline.camera import PCOCamLink
from daolite.pipeline.calibration import PixelCalibration
from daolite.compute import hardware
import numpy as np

# Create pipeline
pipeline = Pipeline()

# Define compute resources
cpu = hardware.amd_epyc_7763()

# Camera parameters
n_pixels = 1024 * 1024
camera_groups = 10

# Pixel calibration agenda
pixel_agenda = np.ones(camera_groups, dtype=int) * (n_pixels // camera_groups)

# Add camera
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CAMERA,
    name="Camera",
    compute=cpu,
    function=PCOCamLink,
    params={
        "n_pixels": n_pixels,
        "group": camera_groups,
        "readout": "rolling"
    }
))

# Add pixel calibration
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Pixel Calibration",
    compute=cpu,
    function=PixelCalibration,
    params={
        "pixel_agenda": pixel_agenda,
        "bit_depth": 16,
        "n_workers": 1
    },
    dependencies=["Camera"]
))

# Run pipeline
results = pipeline.run()
print(f"Calibration time: {results['Pixel Calibration'].duration:.2f} µs")

GPU Acceleration

You can model GPU-accelerated pixel calibration by using a GPU compute resource:

from daolite.compute import hardware

# Use GPU instead of CPU
gpu = hardware.nvidia_rtx_4090()

# Add GPU-accelerated calibration
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="GPU Calibration",
    compute=gpu,  # Use GPU resource
    function=PixelCalibration,
    params={
        "pixel_agenda": pixel_agenda,
        "bit_depth": 16,
        "n_workers": 1
    },
    dependencies=["Camera"]
))

Tuning Performance

You can tune the performance model using the flop_scale and mem_scale parameters:

# Scale computational intensity
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Tuned Calibration",
    compute=cpu,
    function=PixelCalibration,
    params={
        "pixel_agenda": pixel_agenda,
        "bit_depth": 16,
        "flop_scale": 1.2,  # Increase FLOPs by 20%
        "mem_scale": 0.8,   # Reduce memory ops by 20%
        "n_workers": 4      # Use 4 parallel workers
    },
    dependencies=["Camera"]
))

API Reference

For complete API details, see the Calibration API Reference section.

Chunked Processing

For very large pixel arrays, chunked processing can be more efficient:

# Configure chunked processing
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Chunked Calibration",
    compute=cpu,
    function=PixelCalibration,
    params={
        "n_pixels": 4096*4096,  # 16 megapixel array
        "operations": ["dark_subtract", "flat_field"],
        "use_chunking": True,
        "chunk_size": 1024*1024  # Process in 1 megapixel chunks
    },
    dependencies=["Camera"]
))

Customizing Calibration Models

daolite allows you to create custom calibration operations with your own timing models:

def custom_calibration(compute, n_pixels, extra_param=1.0, debug=False):
    """
    Custom calibration function with timing model.

    Args:
        compute: Compute resource
        n_pixels: Number of pixels to process
        extra_param: Custom scaling parameter
        debug: Enable debug output

    Returns:
        Numpy array with timing information
    """
    # Model timing based on compute resource
    if compute.hardware == "GPU":
        # GPU timing model
        bytes_per_pixel = 4  # Float32
        total_bytes = n_pixels * bytes_per_pixel * 2  # Read + write
        mem_time = total_bytes / compute.memory_bandwidth

        # GPU kernels typically need some launch overhead
        kernel_overhead = 5.0  # microseconds
        compute_time = n_pixels * 0.5 / compute.flops + kernel_overhead
    else:
        # CPU timing model
        bytes_per_pixel = 4  # Float32
        total_bytes = n_pixels * bytes_per_pixel * 2  # Read + write

        mem_bandwidth = (compute.memory_channels *
                       compute.memory_width *
                       compute.memory_frequency / 8)  # Bytes/sec
        mem_time = total_bytes / mem_bandwidth

        # CPU model with core utilization
        ops_per_pixel = 10  # Example number of operations per pixel
        total_ops = n_pixels * ops_per_pixel
        compute_time = total_ops / (compute.cores *
                                  compute.core_frequency *
                                  compute.flops_per_cycle)

    # Scale by custom parameter
    compute_time *= extra_param

    # Memory or compute bound?
    processing_time = max(mem_time, compute_time)

    if debug:
        print(f"Memory time: {mem_time} µs")
        print(f"Compute time: {compute_time} µs")
        print(f"Total processing time: {processing_time} µs")

    # Create timing array
    timing = np.zeros([1, 2])
    timing[0, 1] = processing_time

    return timing

# Use custom calibration in a pipeline
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Custom Calibration",
    compute=cpu,
    function=custom_calibration,
    params={
        "n_pixels": 1024*1024,
        "extra_param": 1.2,
        "debug": True
    },
    dependencies=["Camera"]
))

Real-World Applications

Example: High-speed AO System

Calibration configuration for a high-speed adaptive optics system:

# High-speed system with optimized calibration
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Fast Calibration",
    compute=gpu,  # Use GPU for speed
    function=PixelCalibration,
    params={
        "n_pixels": 128*128,  # Small format sensor
        "operations": ["dark_subtract"],  # Minimal processing
        "use_gpu_kernels": True,
        "use_async": True  # Use asynchronous processing
    },
    dependencies=["Camera"]
))

Example: High-precision System

Calibration configuration for a high-precision AO system:

# High-precision system with comprehensive calibration
pipeline.add_component(PipelineComponent(
    component_type=ComponentType.CALIBRATION,
    name="Precision Calibration",
    compute=cpu,
    function=PixelCalibration,
    params={
        "n_pixels": 2048*2048,
        "operations": ["dark_subtract", "flat_field", "bad_pixel",
                      "background", "threshold", "normalize"],
        "window_size": 5,  # Larger window for better correction
        "bad_pixel_fraction": 0.02,
        "threshold": 50
    },
    dependencies=["Camera"]
))

API Reference

For complete API details, see the Calibration API Reference section.