Astronomical Adaptive Optics

Dr. Dobb's Journal April 2004

The Reconstructor algorithm and RTLinux do the job

By Thomas G. Schneider

Tom is the proprietor of Riptide Realtime and develops software and hardware for real-time control applications. He can be contacted at tgs@riptiderealtime.com.

Astronomical adaptive optics is a technique for the real-time removal of the effects of atmospheric distortion from astronomical images. When viewing stars and other astronomical sources of light, this earth-based technique rivals the Hubble Space Telescope for clarity of images. In this article, I describe the real-time part of Adoptics, an application I wrote that currently runs on the MWI and UnISIS systems, which use the historic 100-inch Hooker telescope (Figure 1) at the Mt. Wilson Observatory near Los Angeles, California.

Both of these adaptive optic (AO) systems use a reference point source ("guide star"), which may be either a natural star or artificially produced star created by projecting a high-power pulsed ultraviolet laser beam 18 km into the atmosphere. A point-source guide star is necessary because its theoretically flat wavefront provides a base against which to measure atmospheric distortion. The system's optics project the incoming image from the telescope onto a wavefront sensor so that the distortions in the image's wavefront are measurable; see Figure 2. A correction vector is then calculated from the wavefront distortion data and applied to an electrically deformable mirror (DM), which changes shape to remove the distortion from the optical image. (The deformable mirror technology used in the Mt. Wilson AO systems uses a two-dimensional array of piezoelectric elements bonded to a thin reflective faceplate. The elements, or actuators, have a stroke of a few microns at about 100V, and can push or pull on the faceplate. This recently declassified technology was developed primarily for military applications, but is now available in commercial products.) The corrected optical image, which may be an object near the guide star or the guide star itself, is captured by the science-imaging camera.

The wavefront sensor uses the Shack-Hartmann method to measure wavefront distortion. The incoming wavefront is separated into a two-dimensional array of subapertures by an array of miniature lenslets (see Figure 3). The light passing through each lenslet is brought to a focus on a 2×2-pixel area of a CCD sensor. The position of the focal spot on this 2×2 area is the measure of the gradient, or tilt, of the incoming wavefront. A reconstructor algorithm uses these gradients to calculate the wavefront correction vector and sends it to the DM. The reconstructor algorithm is executed repeatedly in a process called the "control" or "reconstructor" loop. The faster this loop runs, the faster the system can correct for optical distortion. The bandwidth of the control loop is a critical factor in determining the performance of AO systems.

The original Adoptics program I wrote is still running with the ADOPT AO system at Mt. Wilson Observatory. This natural guide star system was designed for the 100-inch Hooker telescope by Dr. Christopher Shelton and became operational in 1995. In the ADOPT system, Adoptics runs under OS/2 on an Intel 486 PC, and uses eight Texas Instruments C40 DSP modules. The control loop runs at 330 Hz, and its speed is limited mainly by the DSP system's throughput. The ADOPT system has produced excellent results, as the difference between the two 1998 images of the triple star 52 Herculis in Figures 4 and 5 illustrate. However, the system's maintainability and upgradeability are limited because of the demise of OS/2, and the temperamental and obsolete DSP hardware.

New AO System Software Requirements

Several years ago the UnISIS AO system became operational at Mt. Wilson. This AO system was designed by Dr. Laird Thompson of the University of Illinois at Urbana-Champaign, and works with either a natural guide star or laser-produced artificial guide star. The UnISIS system uses both a 26×26 (676) pixel and a 40×40 (1600) pixel wavefront sensor, and a 177-element DM. It requires a control-loop frequency of at least 330 Hz, must be able to switch quickly between wavefront sensors, and must be able to take periodic samples from one sensor while running a control loop with the other. Unfortunately, the original Adoptics software could not satisfy all of these requirements, and could run the control loops in only a limited way. Consequently, it was clear I needed to rewrite the program.

In addition to meeting the special requirements of the UnISIS system, any new AO system software would need to support features such as:

Since processor and bus speeds have increased dramatically and RTLinux from FSMLabs (http://www.fsmlabs.com/) has become available, all of these features can now be supported on a Pentium-class PC without requiring any costly DSP hardware. RTLinux supports low-interrupt latency and real-time performance, and Linux's reliability, built-in connectivity, and supportive user community make it an attractive operating system for jobs like this. The 2.4-GHz Pentium processor with its large on-chip cache provides sufficient arithmetic throughput, and fast bus speeds offer good bus-mastering (DMA) performance for wavefront sensor and DM I/O. Plus the PC platform provides a low-cost computer system that's easily serviceable with off-the-shelf components.

With all this in mind, I've completely rewritten the Adoptics software as an RTLinux application, and it is now running on the UnISIS system. The most significant change I made was the simplification of the control-loop algorithm to run on a single-processor system. In the process, the high software overhead of synchronizing and managing data transfers between eight DSPs, which almost defeated the advantage of parallel processing, was eliminated.

New Software Architecture

The RTLinux implementation of Adoptics consists of a control-loop server application and GUI client that can run on the same computer or communicate over Ethernet via the TCP/IP protocol. The control-loop server embodies a real-time reconstructor thread for performing I/O and calculating the correction vector, and must run as an RTLinux thread. In this article, I describe the control-loop server and its real-time thread. The GUI client application does not require real-time operation, and is currently implemented as a Linux X Windows GUI. Moreover, the client/server architecture and Internet connectivity let me operate the system remotely, meaning fewer trips to the observatory.

The control-loop server comprises user-space and kernel-space code, and acts on requests from the GUI client. Figure 6 shows control and data flow between user and kernel space when the real-time thread is running. The kernel-space code is implemented as a Linux device driver, and consists of real-time and nonreal-time code. The user-space code handles mostly server functions, and makes Linux device control (ioctl) calls to the kernel code. The nonreal-time part of the kernel-space code (not shown) handles ioctl calls, diagnostic operations, and static data initialization for the reconstructor algorithm. The real-time thread is implemented as an interrupt service routine (ISR), which gains control when the wavefront sensor interface generates a data-ready interrupt. RTLinux FIFOs are used for communication between user space and the real-time thread.

The Reconstructor Algorithm

The reconstructor algorithm requires static data, including:

Listing One is a C implementation of the reconstructor algorithm. For readability, I've removed the intensity offset vector and gradient offset vector derivation code. The calls to the RTLinux routine gethrtime() (get high-resolution time) return the time in nanoseconds since system boot, and are used to measure the elapsed time of each task. Here are the basic steps of the reconstructor algorithm:

1. Read wavefront sensor CCD image data from its PCI interface's DMA area. The data is converted from the CCD's format to a raster format, and a dark frame is subtracted.

2. Calculate a subaperture gradient vector from the image data. If an image subaperture's cells are labeled as:

A B

D C

the simplified gradient calculation equations are:

X Gradient = (B+C)-(A+D)

Y Gradient = (C+D)-(A+B)

In practice, a gradient offset vector is factored in to these equations. Normalization of the gradient offset vector, needed to account for image intensity changes, is achieved by factoring in the sum of all image pixel intensities during postmatrix-multiplication calculations.

3. Multiply the control matrix by the gradient vector to produce the output vector.

4. Perform type 1 servo calculations on the output vector. The servo's gain and damping are determined by user-space parameters passed during the user-space request call-back routine described in Step 6.

5. Add the DM actuator zero vector to the output vector and send the result to the DM interface's DMA area.

6. Check the RTLinux command FIFO for pending user-space requests by calling a preregistered call-back routine; see Listing Two. The rtf_flush, rtf_get, and rtf_put calls in the code operate on RTLinux FIFOs. Some data buffers are indexed by ModuleIndex because there are two wavefront sensors in the UnISIS system, each with its own unique reconstructor thread code. These threads share the call-back routine, and ModuleIndex identifies the calling thread.

Timings

Figure 7 shows the process timings for Adoptics running the UnISIS control loop in laser guide star mode. The wavefront sensor can deliver an image in about 500 s and the reconstructor algorithm runs in 252 ms. DMA output to the DM takes about 175 ms, so the minimum update period of the control loop is about 1 ms, which is a rate of 1 kHz. In practice, the loop runs at 330 Hz, which corresponds to a period of about 3 ms. Since the reconstructor algorithm's real-time thread only requires 252 ms, about 90 percent of CPU time remains available to the operating system.

The wavefront sensor takes about 500 s to transfer only 676 pixels to DMA memory. The transfer time is limited by the sensor's maximum pixel rate, not by the DMA hardware. Likewise, it takes 175 µs to transfer only 177 actuator values to the DM. This limitation is due to the DM driver hardware's maximum data rate.

Conclusion

The Adoptics application provides a practical and economical solution for operation of an adaptive optics system's reconstructor control loop.

By taking advantage of RTLinux and today's PC hardware, Adoptics gives users real-time reconstructor loop control with visual feedback, and provides many useful support features to aid in aligning optics, analyzing images, and testing and diagnosing system software and hardware.

Acknowledgments

Dr. Christopher Shelton coarchitected the original Adoptics DSP software and introduced me to adaptive optics. Dr. Nils Turner took the two images of 52 Herculis, and the photo of the 100-inch Hooker telescope is by Steve Padilla.

DDJ

Listing One

// LGSReconstructorThread
// This routine is called on receiving an interrupt from the wavefront sensor
// PCI interface board.
// Returns: 0 if OK
//          -EFAULT if external resources not initialized 
// NOTE : Runs in real time - NO SYSTEM CALLS ALLOWED!!!

int LGSReconstructorThread(void)
{
    int i = 0, Row =0;
    float A, B, C, D, G, T, V, RowCounter, Accumulator, Average;
    float * InVecP = 0, * P = 0, * GPtr = 0, * GOVPtr = 0; 
    hrtime_t StartTime;
    // Bail out if pointers, buffers, etc. not initialized  
    if (LGSInitialized == 0)
        return -E;
    StartTime = gethrtime();
    // Acquire, rasterize & offset intensity data
    for (i = 0; i < LGSPixelCount; i++)
    {
        LGSIntensityBuffer[i] = LGSCameraBuffer[LGSCameraMap[i]]
                                - LGSIntensityOffsetVector[i];
    }
    LGSStatus.LoopTiming.FrameAcquired = 
                                 (unsigned long)(gethrtime() - StartTime);
    // Calculate gradients - For future performance increase use pre-built
    // offset tables for indexing in buffers
    // Orientation of ABCD values imposed on intensity buffer:
    //   A | B
    //   - + -
    //   D | C
    // The gradient vector is organized in XY pairs in raster order, as
    // follows:
    //   0 - Row  0, Col  0 X
    //   1 - Row  0, Col  0 Y
    //   2 - Row  0, Col  1 X
    // ...
    //  12 - Row  0, Col 12 Y
    //  13 - Row  1, Col  0 X
    // ...
    // 168 - Row 12, Col 12 X
    // 168 - Row 12, Col 12 Y
     
    GPtr = LGSGradientBuffer;
    GOVPtr = LGSGradientOffsetVector;
    for (i = 0; i < LGS_XYGRAD_COUNT / 2; i++)
    {
        Row = (i / LGS_GCOL_COUNT) * LGS_GCOL_COUNT; 
        A = LGSIntensityBuffer[(Row + i) * 2];
        B = LGSIntensityBuffer[(Row + i) * 2 + 1];
        C = LGSIntensityBuffer[(Row + LGS_GCOL_COUNT + i) * 2 + 1];
        D = LGSIntensityBuffer[(Row + LGS_GCOL_COUNT + i) * 2];            
        *(GPtr++) = (C + B) * *(GOVPtr++) - (D + A);
        *(GPtr++) = (D + C) * *(GOVPtr++) - (A + B);
    }
    LGSStatus.LoopTiming.GradientsDone = 
                              (unsigned long)(gethrtime() - StartTime);
    // Multiply control matrix by gradient vector
    for (i = 0; i < LGSActiveActuatorCount; i++)
    {
        T = 0;
        P = LGSControlMatrix + i * LGS_XYGRAD_COUNT;
        InVecP = LGSGradientBuffer;
        RowCounter = LGS_XYGRAD_COUNT;
        while (RowCounter--)
        {
            T += *InVecP++ * *P++;
        }
        LGSOutputBuffer[i] = T;
    }
    LGSStatus.LoopTiming.MatMulDone = 
                          (unsigned long)(gethrtime() - StartTime);
    // Type 1 integrating servo
    G = LGSLoopParams.DMGain;
    D = LGSLoopParams.DMDamping;
    A = LGSLoopParams.DMUpperLimit;
    B = LGSLoopParams.DMLowerLimit;
    Accumulator = 0;
    for (i = 0; i < LGSActiveActuatorCount; i++)
    {
        V = G * LGSOutputBuffer[i]
            + (1 - D) * LGSTMinus1Buffer[i]
            + D * LGSActuatorOffsetVector[i];
        V = LGSClamp(V, A, B); 
        Accumulator += V;
        LGSOutputBuffer[i] = V;
    }
    LGSStatus.LoopTiming.ServoDone = (unsigned long)(gethrtime() - StartTime);
    // Normalize, clamp and set TMinus1, move to final buffer
    // as 16-bit signed integers
    Average = Accumulator / (float)LGSActiveActuatorCount;
    for (i = 0; i < LGSActiveActuatorCount; i++)
    {
        LGSOutputBuffer[i] -= Average;
        LGSOutputBuffer[i] = LGSClamp(LGSOutputBuffer[i], A, B);
        LGSTMinus1Buffer[i] = LGSOutputBuffer[i];
        // OR into mapped Xinetics buffer, since control bits are there
        DMBufferStart[ToXiMap[i]] |= (int16_t)LGSOutputBuffer[i];
    }
    LGSStatus.LoopTiming.PostServoDone = 
                               (unsigned long)(gethrtime() - StartTime);
    // Output
    DMDriverStartDMA();
    LGSStatus.LoopTiming.DMOutputDone = 
                               (unsigned long)(gethrtime() - StartTime);
    // Keep track of loop passes
    LGSStatus.LoopCount = ++LoopCount;
    // Process runtime requests if a request processor is registered
    if (RequestProcessor)
        RequestProcessor();
    return 0;
}

Back to Article

Listing Two

// ProcessRequests - Handle requests made during loop runtime
// Returns: 0 - No request pending
//          Received command value if request received
// NOTE : Runs in real-time - NO SYSTEM CALLS ALLOWED!!!

int ProcessRequests(void)
{
    uint32_t Command, PayloadCount;
    uint32_t CommandBuffer;
    // Check command FIFO for complete requests and don't continue until
    // one has arrived
    if (ReadFIFOStream(CMD_FIFO,
                       (char *)&CommandBuffer,
                       sizeof(CommandBuffer)) == 0)
        return 0;
    // Extract request command and data count
    Command = CommandBuffer & COMMAND_MASK;
    PayloadCount = (CommandBuffer & COUNT_MASK) >> 16;
    // Process requests
    switch (Command)
    {
    case CMD_GET_ALL_DATA:
        rtf_flush(CMD_RESPONSE_FIFO);
        rtf_put(CMD_RESPONSE_FIFO, (char *)pStatus[ModuleIndex],
                sizeof(LOOP_STATUS));
        rtf_put(CMD_RESPONSE_FIFO, (char *)IntensityBuffer[ModuleIndex],
                IntensityBufferByteCount[ModuleIndex]);
        rtf_put(CMD_RESPONSE_FIFO, (char *)GradientBuffer[ModuleIndex],
                GradientBufferByteCount[ModuleIndex]);
        rtf_put(CMD_RESPONSE_FIFO, (char *)OutputBuffer[ModuleIndex],
                OutputBufferByteCount[ModuleIndex]);
        break;
    case CMD_GET_IOV_FRAME_COUNTER:
        rtf_flush(CMD_RESPONSE_FIFO);
        rtf_put(CMD_RESPONSE_FIFO, (char *)&IOVFrameCounter,
                sizeof(IOVFrameCounter));
        break;
    case CMD_GET_GOV_FRAME_COUNTER:
        rtf_flush(CMD_RESPONSE_FIFO);
        rtf_put(CMD_RESPONSE_FIFO, (char *)&GOVFrameCounter,
                sizeof(GOVFrameCounter));
        break;
    case CMD_SET_LOOP_PARAMS:
        if (rtf_get(CMD_DATA_FIFO, (char *)LoopParams[ModuleIndex],
                    sizeof(LOOP_PARAMS)) == 0)

           // Errors sent back to user space through ERROR_FIFO channel
            SendError(-EINVAL, "No loop parameters provided");
        break;
    case CMD_STOP_LOOP:
        break;
    default:
        break;
    }
    return Command;
}

Back to Article