/* $Id: vmbias.c,v 1.8 2013-07-11 11:44:09 cgarcia Exp $
 *
 * This file is part of the VIMOS Pipeline
 * Copyright (C) 2002-2005 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/*
 * $Author: cgarcia $
 * $Date: 2013-07-11 11:44:09 $
 * $Revision: 1.8 $
 * $Name: not supported by cvs2svn $
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif


#include <string.h>
#include <math.h>

#include <cxmemory.h>

#include <cpl.h>

#include <pilmemory.h>
#include <pilerrno.h>
#include <piltranslator.h>
#include <pilmessages.h>
#include <cpl_msg.h>
#include <pildfsconfig.h>
#include <pilframeset.h>
#include <pilrecipe.h>
#include <pilqc.h>
#include <pilutils.h>

#include "vmimage.h"
#include "vmtable.h"
#include "vmccdtable.h"
#include "vmutils.h"
#include "vmimgpreprocessing.h"
#include "vmimgutils.h"
#include "vmqcutils.h"
#include "vmcpl.h"
#include "fors_dfs.c"


static cxint vmbias(PilSetOfFrames *);


/*
 * Definition of the label strings for all methods the recipe function
 * supports for combining frames and their associated method code.
 */

static const cxchar *methodNames[] = {
  "Auto",
  "Ksigma",
  "MinMax",
  "Median",
  "Average"
};

static const CombMethod methods[] = {
  COMB_AUTO,
  COMB_KSIGMA,
  COMB_REJECT,
  COMB_MEDIAN,
  COMB_AVERAGE
};


/*
 * Create the recipe instance, i.e. setup the parameter list for this
 * recipe and make it availble to the application using the interface.
 */

static cxint
vmbias_create(cpl_plugin *plugin)
{

    cpl_recipe *recipe = (cpl_recipe *)plugin;

    cpl_parameter *p;

    cxint status = 0;


    /*
     * Create the list of options we accept and hook it into the
     * recipe interface.
     */

    recipe->parameters = cpl_parameterlist_new();
    if (recipe->parameters == NULL) {
        return 1;
    }

    
    /*
     * Fill the parameter list
     */

    p = cpl_parameter_new_value("vimos.Parameters.stacking.singleframes",
                                CPL_TYPE_BOOL,
                                "Frame combination method is ignored.",
                                "vimos.Parameters",
                                TRUE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "AllowSingleFrames");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "AllowSingleFrames");
    cpl_parameterlist_append(recipe->parameters, p);

    
    p = cpl_parameter_new_enum("vimos.Parameters.stacking.method",
                               CPL_TYPE_STRING,
                               "Frame combination method",
                               "vimos.Parameters",
                               "Median", 5, "Average", "Median", "MinMax",
                               "Ksigma", "Auto");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "StackMethod");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "StackMethod");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.stacking.ksigma.low",
                                CPL_TYPE_DOUBLE,
                                "Low threshold for K-sigma clipping method",
                                "vimos.Parameters",
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "KSigmaLow");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "KSigmaLow");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.stacking.ksigma.high",
                                CPL_TYPE_DOUBLE,
                                "High threshold for K-sigma clipping method",
                                "vimos.Parameters",
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "KSigmaHigh");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "KSigmaHigh");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.stacking.minmax.minimum",
                                CPL_TYPE_INT,
                                "Number of lowest rejected values for "
                                "rejection method",
                                "vimos.Parameters",
                                1);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "MinRejection");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "MinRejection");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.stacking.minmax.maximum",
                                CPL_TYPE_INT,
                                "Number of highest rejected values for "
                                "rejection method",
                                "vimos.Parameters",
                                1);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "MaxRejection");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "MaxRejection");
    cpl_parameterlist_append(recipe->parameters, p);

#ifdef ONLINE_MODE

    p = cpl_parameter_new_value("vimos.Parameters.bias.frames.validate",
                                CPL_TYPE_BOOL,
                                "Consistency check on raw bias frames",
                                "vimos.Parameters",
                                TRUE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "ValidateFrames");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "ValidateFrames");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("vimos.Parameters.bias.tolerance.level",
                                CPL_TYPE_DOUBLE,
                                "Threshold for level consistency",
                                "vimos.Parameters",
                                3.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "LevelTolerance");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "LevelTolerance");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.bias.tolerance.pattern",
                                CPL_TYPE_DOUBLE,
                                "Threshold for flux pattern consistency",
                                "vimos.Parameters",
                                3.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "PatternTolerance");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "PatternTolerance");
    cpl_parameterlist_append(recipe->parameters, p);

#endif

    p = cpl_parameter_new_value("vimos.Parameters.bias.overscan.remove",
                                CPL_TYPE_BOOL,
                                "Remove overscan regions from master bias",
                                "vimos.Parameters",
                                TRUE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "RemoveOverscan");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "RemoveOverscan");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.bias.badpixel.clean",
                                CPL_TYPE_BOOL,
                                "Bad pixel correction on master bias",
                                "vimos.Parameters",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "CleanBadPixel");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "CleanBadPixel");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.bias.cosmics.clean",
                                CPL_TYPE_BOOL,
                                "Cosmic ray removal from each raw bias",
                                "vimos.Parameters",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "CleanCosmic");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "CleanCosmic");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.bias.quality.enable",
                                CPL_TYPE_BOOL,
                                "Compute QC1 parameters",
                                "vimos.Parameters",
                                TRUE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "ComputeQC");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "ComputeQC");
    cpl_parameterlist_append(recipe->parameters, p);

#ifdef ONLINE_MODE

    p = cpl_parameter_new_value("vimos.Parameters.bias.quality.apply",
                                CPL_TYPE_BOOL,
                                "Quality control of master bias",
                                "vimos.Parameters",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "ApplyQC");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "ApplyQC");
    cpl_parameterlist_append(recipe->parameters, p);


    p = cpl_parameter_new_value("vimos.Parameters.bias.quality.maxoffset",
                                CPL_TYPE_DOUBLE,
                                "Maximum allowed deviation from nominal "
                                "bias level",
                                "vimos.Parameters",
                                3.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "MaxDeviation");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CFG, "MaxDeviation");
    cpl_parameterlist_append(recipe->parameters, p);

#endif

    /*
     * Initialize the VIMOS recipe subsystems (configuration data base,
     * alias tables, messaging facilities) from the current CPL setup.
     */

    status = vmCplRecipeStart(cpl_plugin_get_name(plugin), VERSION);

    if (status) {
        return 1;
    }

    return 0;

}


/*
 * Execute the plugin instance given by the interface.
 */

static cxint
vmbias_exec(cpl_plugin *plugin)
{

    cpl_recipe *recipe = (cpl_recipe *)plugin;

    cxint status = 0;

    PilSetOfFrames *sof = NULL;


    if (recipe->parameters == NULL || recipe->frames == NULL) {
        return 1;
    }


    /*
     * Convert recipe inputs
     */

    sof = newPilSetOfFrames();

    if (sof == NULL) {
        return 1;
    }

    status = vmCplFramesetExport(recipe->frames, sof);

    if (status) {
        deletePilSetOfFrames(sof);
        return 1;
    }

    status = pilRecValidateSet(sof);

    if (!status) {
        deletePilSetOfFrames(sof);
        return 1;
    }

    status = vmCplParlistExport(recipe->parameters);

    if (status) {
        deletePilSetOfFrames(sof);
        return 1;
    }


    /*
     * Execute the data reduction task.
     */

    vmCplRecipeTimerStart(NULL);

    if (vmbias(sof) == EXIT_SUCCESS) {

        /*
         * Post process the product frames registered in the set
         * of frames.
         */

        status = vmCplPostProcessFrames(sof, "vmbias");
        
        if (status == 0) {

            /*
             * Update recipe interface with the product frames found in 'sof'
             * and destroy it.
             */

            status = vmCplFramesetImport(recipe->frames, sof);
        }

    }
    else
        status = 1;

    vmCplRecipeTimerStop(NULL);

        
    /*
     * Release locally acquired resources
     */

    deletePilSetOfFrames(sof);
    
    return status == 0 ? 0 : 1;

}


static cxint
vmbias_destroy(cpl_plugin *plugin)
{

    cpl_recipe *recipe = (cpl_recipe *)plugin;


    /*
     * Stop the VIMOS recipe subsystems first.
     */

    vmCplRecipeStop();


    /*
     * We just destroy what was created during the plugin initialization
     * phase, i.e. the parameter list. The frame set is managed by the 
     * application which called us, so we must not touch it.
     */

    if (recipe->parameters != NULL) {
        cpl_parameterlist_delete(recipe->parameters);
    }

    return 0;

}


/* 
 * @brief
 *   Create a master bias from set of raw bias frames.
 *
 * @return The function returns @c EXIT_SUCCESS if no error occurred,
 *   otherwise the function returns @c EXIT_FAILURE.
 *
 * @param sof   Set of frames containing the references to
 *              raw bias images and (optionally) a CCD Table.
 *
 * The recipe function creates a master bias frame from a set of
 * raw bias frames. The raw frames are passed via the set of
 * frames @b sof. On successful termination the created master
 * bias is added to this set of frames.
 * 
 * Control options and additional parameters are read from the recipe
 * configuration database. The recipe function accepts the following
 * task parameters:
 * @li StackMethod
 * @li KSigmaLow
 * @li KSigmaHigh
 * @li MinRejection
 * @li MaxRejection
 * @li ValidateFrames
 * @li LevelTolerance
 * @li PatternTolerance
 * @li RemoveOverscan
 * @li CorrectBadPixel
 * @li CorrectCosmic
 * @li ApplyQC
 * @li MaxDeviation
 * If any of these task parameters is not set in the recipe configuration
 * database the recipe function uses the builtin defaults for these
 * task parameters.
 * 
 * The master bias is created by combining the raw bias frames using
 * one of the following combination methods:
 * @li Average of frames
 * @li Median stacking
 * @li Minimum-Maximum rejection
 * @li Kappa-Sigma clipping
 * @li Automatic (not yet available)
 * The default combination method is 'Automatic'. This method determines
 * the number of available raw bias frames and selects the method
 * giving the best possible result (currently this is not implemented,
 * it falls back to average).
 * 
 * The recipe function can be configured to ignore input frames showing
 * a bias level and/or image pattern that is inconsistent with the
 * majority of the input frames. This consistency check is controlled
 * by the task parameter @b ValidateFrames. The default is to perform
 * a validation of the input frames. Bias frames are ignored if their bias
 * level is not within @b LevelTolerance sigma of the median bias level
 * measured across all raw bias frames. The variance used for this bias
 * selection is computed as the average standard deviation from the median
 * bias level. A bias frame is also excluded if the image pattern is
 * different compared to the other bias frames. This is measured by comparing
 * the deviation from 0 of the median pixel value of the difference image
 * with @b PatternTolerance times the expected noise.
 * 
 * The function allows to remove or keep the pre- and overscan regions
 * in the created master bias frame. The removal of the pre- and overscan
 * regions is controlled by the task parameter @b RemoveOverscan.
 * The default is to remove the pre- and overscan areas.
 * 
 * Optionally the input raw bias frames may be corrected for cosmic
 * ray events. The cosmic ray removal is controlled by the task 
 * parameter @b CorrectCosmic. In case a CCD_TABLE is also given, bad
 * pixels will not be used in computing the interpolated values to replace
 * the cosmic rays events with. The default is not to correct for cosmic
 * ray events.
 * 
 * Optionally the created master bias frame may be corrected for bad
 * pixels. If this option is turned on, the recipe expects to find
 * a CCD_TABLE in the input set of frames. The bad pixel correction
 * is controlled by the task parameter @b CorrectBadPixel. The
 * default is not to correct for bad pixels.
 * 
 * If the task parameter @b ApplyQC is set to @b true the recipe
 * function applies a simple quality control task to the created
 * master bias frame. The check compares the bias level of the master
 * bias with the nominal bias value taken from the header of a reference
 * master bias which is looked up in the set of frames @em sof. If the
 * offset from the nominal level is larger than @b MaxDeviation sigma a
 * warning is issued, but the function does not fail.
 * If this option is turned on the recipe expects to find a master bias in
 * input, or it will return an error.
 */

static cxint
vmbias(PilSetOfFrames *sof)
{

    const char task[] = "vmbias";

    const char *biasTag = pilTrnGetCategory("Bias");
    const char *mbiasTag = pilTrnGetCategory("MasterBias");

    char *methodTag = 0;
    char mbiasName[PATHNAME_MAX + 1];

    size_t  biasCount, goodFrames;
    size_t  minFrames;

    unsigned int i, j;
    unsigned int validateFrames, removeOverscan;
    unsigned int cleanBadPixel, cleanCosmic, computeQC, applyQc;
    unsigned int singleFrames;

    int methodEntry;

    float *biasLevel = 0;
    float *biasNoise = 0;

    double medianLevel, meanNoise;
    double levelTolerance, levelThreshold, lowerThreshold, upperThreshold;
    double patternTolerance;
    double maxDeviation;

    PilFrame *currFrame, *ccdFrame, *rbiasFrame, *productFrame;

    CombMethod method = COMB_UNDEF;
    CombParameters combParameter;

    VimosImage *mBias = 0;
    VimosImage *rBias = 0;
    VimosImage **biasList, **refList;

    VimosTable *ccdTable = 0;


    /*
     * Get task parameters from the recipe database
     */

    /*
     * Determine the frame stacking method first and all
     * method dependent parameters.
     */
  
    methodTag = (char *)pilDfsDbGetString("Parameters", "StackMethod");
    methodEntry = strselect(methodTag, methodNames, PIL_N_ELEMENTS(methods));

    if (methodEntry < 0) {
        cpl_msg_error(task, "%s: Invalid frame combination method.", methodTag);
        return EXIT_FAILURE;
    }
    else {
        method = methods[methodEntry];
    }

    switch (method) {
    case COMB_KSIGMA:
        minFrames = MIN_FRAMES_KSIGMA;
        combParameter.kSigmaLow = pilDfsDbGetDouble("Parameters",
                                                    "KSigmaLow", 5.0);
        combParameter.kSigmaHigh = pilDfsDbGetDouble("Parameters",
                                                     "KSigmaHigh", 5.0);
        break;

    case COMB_REJECT:
        minFrames = MIN_FRAMES_REJECT;
        combParameter.minRejection = pilDfsDbGetInt("Parameters",
                                                    "MinRejection", 1);
        combParameter.maxRejection = pilDfsDbGetInt("Parameters",
                                                    "MaxRejection", 1);
        break;

    case COMB_MEDIAN:
        minFrames = MIN_FRAMES_MEDIAN;
        break;

    case COMB_AVERAGE:
        minFrames = MIN_FRAMES_AVERAGE;
        break;

    default:
        cpl_msg_warning(task, "Invalid stacking method. Using default "
                      "method 'Average'!");
        method = COMB_AVERAGE;
        minFrames = MIN_FRAMES_AVERAGE;
        break;
    }

  
    /*
     * Check if a single frame in input should be tolerated. This
     * means that if a single frame is found in input, then the
     * stacking method will be ignored.
     */

    singleFrames = pilDfsDbGetBool("Parameters", "AllowSingleFrames", 0);


    /*
     * Check if raw frames with non-nominal exposure level should be ignored
     * by the image stacking task. Default is to remove non-nominal frames.
     */

    validateFrames = pilDfsDbGetBool("Parameters", "ValidateFrames", 0);


    /*
     * Check if the overscan areas should be removed after stacking the
     * frames.
     */

    removeOverscan = pilDfsDbGetBool("Parameters", "RemoveOverscan", 1);


    /*
     * Check if the bad pixels should be corrected.
     */

    cleanBadPixel = pilDfsDbGetBool("Parameters", "CleanBadPixel", 0);


    /*
     * Check if the cosmic rays events should be corrected.
     */

    cleanCosmic = pilDfsDbGetBool("Parameters", "CleanCosmic", 0);


    /*
     * Check if QC1 parameters should be computed
     */

    computeQC = pilDfsDbGetBool("Parameters", "ComputeQC", 1);


    /*
     * Check if the bias quality control should be applied to the
     * final product.
     */

    applyQc = pilDfsDbGetBool("Parameters", "ApplyQC", 0);
    maxDeviation = pilDfsDbGetDouble("Parameters", "MaxDeviation", 3.0);


    /* 
     * Make sure that there are enough raw data frames in the
     * input set.
     */

    biasCount = (int)pilSofFrameCount(sof, biasTag);

    if (biasCount < minFrames) {
        if (biasCount == 1 && singleFrames) {
            method = COMB_UNDEF;
            methodTag = "Undefined";
            if (computeQC) {
                cpl_msg_warning(task, "QC1 parameters are not computed: at "
                              "least two bias exposures would be required.");
                computeQC = 0;
            }
        }
        else {
            cpl_msg_error(task, "Not enough raw bias frames in input for "
                        "stacking method '%s'!", methodTag);
            return EXIT_FAILURE;
        }
    }


    /*
     * If the bad pixel correction is enabled, a bad pixel table is required
     * in the input set of frames. If no bad pixel table is present this is an
     * error. If cleaning of cosmic rays is enabled, the bad pixel table will
     * be used, if present, to avoid bad pixels while smoothing away cosmic
     * ray events. In this case, a missing bad pixel table is not an error.
     */

    ccdFrame = pilSofLookup(sof, pilTrnGetCategory("CcdTable"));
    if (ccdFrame) 
        pilFrmSetType(ccdFrame, PIL_FRAME_TYPE_CALIB);

    if (cleanBadPixel || cleanCosmic) {
        if (!ccdFrame) {
            if (cleanBadPixel) {
                cpl_msg_error(task, "Bad pixel cleaning requires a CCD "
                            "table in input!");
                return EXIT_FAILURE;
            }
        }
        else {
            cpl_msg_debug(task, "CCD table is %s", pilFrmGetName(ccdFrame));

            if (!(ccdTable = openOldFitsTable(pilFrmGetName(ccdFrame), 0))) {
                cpl_msg_error(task, "Cannot load CCD table %s!",
                            pilFrmGetName(ccdFrame));
                return EXIT_FAILURE;
            }
            else 
                closeFitsTable(ccdTable, 0);
        }
    }


    /*
     * If the quality control check is requested a reference master bias is
     * expected in the input set of frames. If it is not found it is an error.
     */

    rbiasFrame = pilSofLookup(sof, pilTrnGetCategory("MasterBias"));
    if (rbiasFrame)
        pilFrmSetType(rbiasFrame, PIL_FRAME_TYPE_CALIB);

    if (applyQc) {

        if (rbiasFrame == NULL) {
            cpl_msg_error(task, "Product frame quality control requires "
                        "a master bias in input!");
            return EXIT_FAILURE;
        }
        else {
            cpl_msg_debug(task, "Reference bias is %s",
                        pilFrmGetName(rbiasFrame));

            if (!(rBias = openOldFitsFile(pilFrmGetName(rbiasFrame), 0, 0))) {
                cpl_msg_error(task, "Cannot load reference bias %s!",
                            pilFrmGetName(rbiasFrame));
                return EXIT_FAILURE;
            }
            else {
                if (loadFitsHeader(rBias) == VM_FALSE) {
                    cpl_msg_error(task, "Cannot load reference bias header");

                    closeFitsImage(rBias, 0);
                    deleteImage(rBias);

                    if (ccdTable)
                        deleteTable(ccdTable);
                }
                else
                    closeFitsImage(rBias, 0);
            }
        }
    }


    /*
     * Load the raw bias frames.
     */

    biasList = (VimosImage **)pil_calloc(biasCount, sizeof(VimosImage *));
    if (biasList == NULL) {
        cpl_msg_error(task, "Not enough memory!");
        deleteTable(ccdTable);

        return EXIT_FAILURE;
    }

    currFrame = pilSofLookupNext(sof, biasTag);

    for (i = 0; i < biasCount; i++) {

        biasList[i] = openOldFitsFile(pilFrmGetName(currFrame), 1, 0);

        if (biasList[i] == NULL) {
            cpl_msg_error(task, "Cannot load bias frame %d", i + 1);

            for (j = 0; j < i; j++)
                deleteImage(biasList[j]);
            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }
        else {
            pilFrmSetType(currFrame, PIL_FRAME_TYPE_RAW);
            closeFitsImage(biasList[i], 0);
            currFrame = pilSofLookupNext(sof, 0);
        }
    }


    /*
     * Do a preselection of the raw bias frames based on their average
     * median intensity level. A bias frames whose level deviates too
     * much from the average level are removed from the input and are
     * not treated in all following processing steps.
     */

    if (validateFrames && biasCount < 2) {
        validateFrames = 0;
        cpl_msg_warning(task, "Too few bias frames (%zd) in input. Skipping "
                      "frame selection task!", biasCount);
    }

    /*
     * Initially assume that all frames are valid. This is needed to
     * make sure that goodFrames is setup correctly if the tests
     * are skipped.
     */

    goodFrames = biasCount;

    if (validateFrames || cleanCosmic) {

        if (!(biasLevel = (float *)pil_calloc(biasCount, sizeof(float)))) {
            cpl_msg_error(task, "Not enought memory!");

            for (i = 0; i < biasCount; i++)
                deleteImage(biasList[i]);

            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }

        if (!(biasNoise = (float *)pil_calloc(biasCount, sizeof(float)))) {
            cpl_msg_error(task, "Not enough memory!");

            for (i = 0; i < biasCount; i++) {
                deleteImage(biasList[i]);
            }

            pil_free(biasList);
            pil_free(biasLevel);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }


        /*
         * Bias mean level and read out noise must be evaluated if either 
         * cosmic rays cleaning or frame validation was requested.
         */

        cpl_msg_info(task, "Computing bias level(s) ...");

        for (i = 0; i < biasCount; i++) {
            biasLevel[i] = imageMean(biasList[i]);
            biasNoise[i] = computeAverageRon(biasList[i]);

            if (biasNoise[i] < 0.) {
                cpl_msg_warning(task, "Get bias %d RON from keyword header",
                              i + 1);
                biasNoise[i] = getAverageRon(biasList[i]);

                if (biasNoise[i] < 0.) {

                    cpl_msg_error(task, "Cannot compute bias RON!");

                    for (i = 0; i < biasCount; i++) {
                        deleteImage(biasList[i]);
                    }

                    pil_free(biasList);
                    pil_free(biasLevel);

                    deleteTable(ccdTable);

                    return EXIT_FAILURE;
                }

            }

            cpl_msg_info(task, "Level of bias %-d is %10.4f +/- "
                       "%-.4f ADU", i + 1, biasLevel[i], biasNoise[i]);
        }


        /*
         * Remove cosmic ray hits from all input frames
         */

        if (cleanCosmic) {
            cpl_msg_info(task, "Cleaning cosmic ray events...");

            for (i = 0; i < biasCount; i++) {
                if (VmCosmicClean(biasList[i], ccdTable, 0, biasLevel[i], 1.,
                                  biasNoise[i], -1., -1.) == EXIT_FAILURE) {
                    cpl_msg_error(task, "Cannot clean cosmic ray hits from "
                                "raw bias frame %d", i + 1);

                    for (i = 0; i < biasCount; i++)
                        deleteImage(biasList[i]);

                    pil_free(biasList);
                    pil_free(biasNoise);
                    pil_free(biasLevel);

                    deleteTable(ccdTable);
                    return EXIT_FAILURE;
                }
                cpl_msg_info(task, "  Bias %d of %zd done", i + 1, biasCount);
            }
        }


        /*
         * Select valid frames only for the frame combination.
         */

        if (validateFrames) {

            cpl_msg_info(task, "Checking bias levels for consistency ...");

            if (biasCount < 3) {
                medianLevel = computeAverageFloat(biasLevel, biasCount);
            }
            else {
                medianLevel = medianPixelvalue(biasLevel, biasCount);
            }

            meanNoise = computeAverageFloat(biasNoise, biasCount);
            cpl_msg_info(task, "Mean bias noise: %-.4f adu", meanNoise);


            /*  
             * Remove all bias frames from the input list which do not have
             * a bias level comparable to the median bias level of all bias
             * frames in the list. The selection criteria is bound to the 
             * mean noise of the frames.
             */

            levelTolerance = pilDfsDbGetDouble("Parameters", "LevelTolerance",
                                               3.0);
            levelThreshold = levelTolerance * meanNoise;

            cpl_msg_info(task, "Selecting frames with consistent bias "
                       "level ...");
            cpl_msg_info(task, "Valid range for bias levels: %-10.4f +/- "
                       "%-.4f adu", medianLevel, levelThreshold);

            /*
             * Make sure that bias frames having a level smaller than the
             * mean noise are excluded.
             */

            upperThreshold = medianLevel + levelThreshold;
            lowerThreshold = medianLevel - levelThreshold;

            if (lowerThreshold < 0) {
                lowerThreshold = meanNoise;
                cpl_msg_warning(task, "Lower limit bias level would be "
                              "below 0. Lower limit reset to %-.4f adu",
                              lowerThreshold);
            }

            /*
             * The original sorting order of the input bias list must be kept.
             * It is needed later on for changing the sorting order of the
             * value arrays associated to the list, like the level and noise
             * array.
             */

            refList = (VimosImage **)pil_calloc(biasCount,
                                                sizeof(VimosImage *));
            if (!refList) {
                cpl_msg_error(task, "Not enough memory!");

                for (i = 0; i < biasCount; i++)
                    deleteImage(biasList[i]);

                pil_free(biasList);
                pil_free(biasNoise);
                pil_free(biasLevel);

                deleteTable(ccdTable);
                return EXIT_FAILURE;
            }
            else 
                for (i = 0; i < biasCount; i++)
                    refList[i] = biasList[i];


            /*
             * Good images are moved to the beginning of the biasList:
             */

            goodFrames = applyListSelection(biasList, biasLevel, biasCount, 
                                            lowerThreshold, upperThreshold, 1);

            if (goodFrames < biasCount) {
                if (remapFloatsLikeImages(refList, biasList, biasNoise,
                                          biasCount) == EXIT_FAILURE) {
                    cpl_msg_error(task, "Bias frame selection failed!");

                    for (i = 0; i < biasCount; i++)
                        deleteImage(biasList[i]);

                    pil_free(refList);
                    pil_free(biasList);
                    pil_free(biasNoise);
                    pil_free(biasLevel);

                    deleteTable(ccdTable);
                    return EXIT_FAILURE;
                }
            }

            pil_free(refList);

            cpl_msg_info(task, "%zd raw bias frames selected.", goodFrames);


            /*
             * Check if the remaining input frames have the same intensity
             * distribution, i.e., if the input bias frames show a similar
             * illumination pattern. This makes sense only if there is more
             * than one image left.
             */

            if (goodFrames > 1) {
                cpl_msg_info(task, "Checking for consistent signal "
                           "distribution ...");

                patternTolerance = pilDfsDbGetDouble("Parameters",
                                                     "PatternTolerance", 3.0);
                goodFrames = qcSelectConsistentImages(biasList, biasNoise,
                                                      goodFrames,
                                                      patternTolerance);

                if (goodFrames == 0 && pilErrno == P_EGENERIC) {
                    cpl_msg_error(task, "Selection of consistent images "
                                "failed!");
                    return EXIT_FAILURE;
                }

                cpl_msg_info(task, "%zd out of %zd consistent bias frames "
                           "selected from input set", goodFrames, biasCount);
            }
        }

        pil_free(biasLevel);
        pil_free(biasNoise);


        /*
         * Erase inconsistent frames from the rearranged list of bias
         * frames.
         */

        for (i = goodFrames; i < biasCount; i++)
            deleteImage(biasList[i]);

        if (!goodFrames) {
            cpl_msg_error(task, "No bias frames with consistent signal "
                        "distribution found in input. All frames excluded!");

            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }
    }


    /*
     * Stack the selected bias frames, if requested, and if possible.
     */

    if (method == COMB_UNDEF) {
        mBias = duplicateImage(biasList[0]);
    }
    else if (goodFrames >= minFrames) {
        cpl_msg_info(task, "Combining %zd frames with method '%s'", goodFrames,
                   methodNames[methodEntry]);

        mBias = frComb(biasList, goodFrames, method, &combParameter, 0);
    }
    else {
        cpl_msg_error(task, "Not enough good frames for requested stacking "
                    "method '%s'!", methodNames[methodEntry]);
        for (i = 0; i < goodFrames; i++)
            deleteImage(biasList[i]);
        pil_free(biasList);
        deleteTable(ccdTable);
        return EXIT_FAILURE;
    }

    if (!mBias) {
        cpl_msg_error(task, "Stacking of raw bias frames failed! No master "
                    "bias was created!");
 
        for (i = 0; i < goodFrames; i++)
            deleteImage(biasList[i]);
        pil_free(biasList);

        deleteTable(ccdTable);
        return EXIT_FAILURE;
    }


    /*
     * Create the master bias header
     */

    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 pilTrnGetKeyword("MjdObs"), NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 pilTrnGetKeyword("DateObs"), NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 pilTrnGetKeyword("ArchiveFile"), NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^C[A-Z]*[1,2]", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO OBS (DID|PROG ID)", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO OBS ID", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO TPL [.DINPSV.]", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO INS (DID|MODE)", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO DET [A-Z].*", NULL);
    vimosDscCopy(&mBias->descs, biasList[0]->descs,
                 "^ESO OCS (DID|CON QUAD)", NULL);


    /*
     * Bad pixel cleaning of the result
     */

    if (cleanBadPixel) {
        cpl_msg_info(task, "Cleaning bad pixels on result frame ...");

        if (cleanBadPixels(mBias, ccdTable, 0) == EXIT_FAILURE) {
            cpl_msg_error(task, "Bad pixel cleaning failed!");
 
            for (i = 0; i < goodFrames; i++)
                deleteImage(biasList[i]);
            pil_free(biasList);

            deleteImage(mBias);
            deleteTable(ccdTable);

            return EXIT_FAILURE;
        }
    }


    /*
     * Remove overscan areas from the created master bias.
     */
  
    if (removeOverscan) {
        cpl_msg_info(task, "Removing overscan areas from master bias ...");

        if (trimOverscans(mBias) == EXIT_FAILURE) {
            cpl_msg_error(task, "Removing overscan areas from master "
                        "bias failed!");
 
            for (i = 0; i < goodFrames; i++)
                deleteImage(biasList[i]);
            pil_free(biasList);

            deleteTable(ccdTable);
            deleteImage(mBias);

            return EXIT_FAILURE;
        }
    }


    if (computeQC) {

        cpl_msg_info(task, "Computing QC1 parameters...");


        /*
         *  Currently the central 1600x1800 pixels are used
         */

        int winSizeX = 1600;
        int winSizeY = 1800;
        int winStartX = (biasList[0]->xlen - winSizeX) / 2;
        int winStartY = (biasList[0]->ylen - winSizeY) / 2;
        int good;

        unsigned int npix = winSizeX * winSizeY;

        double value, meanValue, ronValue, fpnValue, rmsValue, structValue;
        double threshold;

        float *sample1 = NULL;
        float *sample2 = NULL;
        float *diff = NULL;


        sample1 = extractFloatImage(biasList[0]->data, biasList[0]->xlen,
                biasList[0]->ylen, winStartX,
                winStartY, winSizeX, winSizeY);

        sample2 = extractFloatImage(biasList[1]->data, biasList[1]->xlen,
                biasList[1]->ylen, winStartX,
                winStartY, winSizeX, winSizeY);

        diff = pil_malloc(winSizeX * winSizeY * sizeof(float));

        if (!(sample1 && sample2 && diff)) {
            cpl_msg_error(task, "Memory allocation!");

            pil_free(sample1);
            pil_free(sample2);
            pil_free(diff);

            for (i = 0; i < goodFrames; i++)
                deleteImage(biasList[i]);
            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }

        /*
         *  Compute images difference
         */

        for (i = 0; i < npix; i++)
            diff[i] = sample1[i] - sample2[i];

        pil_free(sample2);

        /*
         * Compute QC parameters.
         */

        /* QC.BIAS.MEAN */

        meanValue = computeAverageFloat(sample1, winSizeX * winSizeY);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MEAN",
                              meanValue, "Mean bias level [ADU]");

        /* QC.RON */

        value = computeVarianceFloat2D(diff, winSizeX, winSizeY);
        ronValue = sqrt(value / 2);

        writeDoubleDescriptor(&mBias->descs, "ESO QC RON",
                              ronValue, "Read out noise [ADU]");

        /* QC.BIAS.FPN */

        sample2 = extractFloatImage(biasList[1]->data,
                biasList[1]->xlen, biasList[1]->ylen,
                winStartX + 10, winStartY + 10, 
                winSizeX, winSizeY);

        if (!sample2) {
            cpl_msg_error(task, "Memory allocation!");

            pil_free(sample1);
            pil_free(diff);

            for (i = 0; i < goodFrames; i++)
                deleteImage(biasList[i]);
            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }

        /*
         *  Compute difference of first bias with shifted second bias.
         */

        for (i = 0; i < npix; i++)
            diff[i] = sample1[i] - sample2[i];

        pil_free(sample2);

        value = computeVarianceFloat2D(diff, winSizeX, winSizeY);
        if (value / 2 > ronValue * ronValue)
            fpnValue = sqrt(value / 2 - ronValue * ronValue);
        else
            fpnValue = 0.0;

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS FPN",
                              fpnValue, "Bias fixed pattern noise [ADU]");

        pil_free(diff);

        value = 0.0;
        for (i = 0; i < npix; i++)
            value += (sample1[i] - meanValue) * (sample1[i] - meanValue);

        value /= npix;
        rmsValue = sqrt(value);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS RMS",
                              rmsValue, "RMS of bias [ADU]");

        if (value > fpnValue * fpnValue + ronValue * ronValue)
            structValue = sqrt(value - fpnValue * fpnValue -
                    ronValue * ronValue);
        else
            structValue = 0;

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS STRUCT",
                              structValue, "Bias structure [ADU]");


        /* QC.BIAS.MEDIAN */

        value = medianPixelvalue(sample1, winSizeX * winSizeY);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MEDIAN",
                              value, "Median bias level [ADU]");

        pil_free(sample1);

        /* QC.BIAS.MASTER.MEAN */

        /*
         * For the master bias the sample window must be recomputed - here 
         * overscans are missing...
         */

        winStartX = (mBias->xlen - winSizeX) / 2;
        winStartY = (mBias->ylen - winSizeY) / 2;

        sample1 = extractFloatImage(mBias->data, mBias->xlen,
                mBias->ylen, winStartX, winStartY,
                winSizeX, winSizeY);

        if (!sample1) {
            cpl_msg_error(task, "Memory allocation!");

            for (i = 0; i < goodFrames; i++)
                deleteImage(biasList[i]);
            pil_free(biasList);

            deleteTable(ccdTable);
            return EXIT_FAILURE;
        }

        meanValue = computeAverageFloat(sample1, winSizeX * winSizeY);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER MEAN",
                              meanValue, "Mean master bias level [ADU]");

        /* QC.BIAS.MASTER.RMS */

        meanValue = computeAverageFloat(sample1, winSizeX * winSizeY);

        value = 0.0;
        for (i = 0; i < npix; i++)
            value += (sample1[i] - meanValue) * (sample1[i] - meanValue);

        value /= npix;
        rmsValue = sqrt(value);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER RMS",
                              rmsValue, "RMS of master bias [ADU]");

        /* QC.BIAS.MASTER.NOISE  &  QC.BIAS.MASTER.FPN */

        /*
         * This is not yet the fixed pattern noise: the ron contribution
         * must still be evaluated and subtracted.
         */

        fpnValue = computeVarianceFloat2D(sample1, winSizeX, winSizeY);

        /*
         * The expected noise is QC.RON / sqrt(goodFrames). From this
         * we set a tolerance against outlyers equal to 3 times this
         * value. The noise is evaluated from the master central region, 
         * excluding outlyers the (supposedly belonging to the fixed 
         * pattern noise). Along the way, the fixed pattern noise is 
         * also evaluated.
         */

        threshold = 3 * ronValue / sqrt((double)goodFrames);

        /*
         * Here the RON contribution is evaluated:
         */

        meanValue = computeAverageFloat(sample1, winSizeX * winSizeY);

        good = 0;
        value = 0.0;
        for (i = 0; i < npix; i++) {
            if (fabs(sample1[i] - meanValue) < threshold) {
                value += (sample1[i] - meanValue) *
                        (sample1[i] - meanValue);
                good++;
            }
        }

        /*
         * Threshold was clearly underestimated, skip it
         */

        if (good == 0) {
            value = 0.0;
            for (i = 0; i < npix; i++)
                value += (sample1[i] - meanValue) *
                (sample1[i] - meanValue);
            good = npix;
        }

        value /= good;  /* Mean variance excluding outlyers */

        if (fpnValue > value)
            fpnValue = sqrt(fpnValue - value);
        else
            fpnValue = 0.0;

        ronValue = sqrt(value);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER NOISE",
                              ronValue, "Noise of master bias [ADU]");

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER FPN",
                         fpnValue, "Fixed pattern noise of master bias [ADU]");

        /* QC.BIAS.MASTER.STRUCT */

        meanValue = computeAverageFloat(sample1, winSizeX * winSizeY);

        value = 0.0;
        for (i = 0; i < npix; i++)
            value += (sample1[i] - meanValue) * (sample1[i] - meanValue);

        value /= npix;

        if (rmsValue * rmsValue > fpnValue * fpnValue +
                ronValue * ronValue) {
            structValue = sqrt(rmsValue * rmsValue -
                    fpnValue * fpnValue - ronValue * ronValue);
        }
        else {
            structValue = 0.0;
        }

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER STRUCT",
                              structValue, "Structure of master bias [ADU]");

        /* QC.BIAS.MASTER.MEDIAN */

        value = medianPixelvalue(sample1, winSizeX * winSizeY);

        writeDoubleDescriptor(&mBias->descs, "ESO QC BIAS MASTER MEDIAN",
                              value, "Median master bias level [ADU]");

        pil_free(sample1);


    } /* End of QC1 computation. */
 
    for (i = 0; i < goodFrames; i++)
        deleteImage(biasList[i]);
    pil_free(biasList);

    /*
     * Apply the quality control check to the created master bias
     * For the master bias creation the QC check will only issue
     * a warning but it will not fail. For the check a reference
     * master bias is required.
     */

    if (applyQc) {
        int state;

        state = qcCheckBiasLevel(mBias, rBias, maxDeviation, 1, 1);

        if (state == EXIT_FAILURE) {
            cpl_msg_error(task, "Quality control of master bias failed!");

            deleteTable(ccdTable);
            deleteImage(mBias);

            return EXIT_FAILURE;
        }
    }


    /*
     * Build the local output file name from its product category.
     */

    strlower(strcpy(mbiasName, mbiasTag));
    strcat(mbiasName, ".fits");


    /*
     * Update the product header.
     */

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("DataMin"),
                           imageMinimum(mBias),
                           pilTrnGetComment("DataMin"),
                           "ESO*", 1);

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("DataMax"),
                           imageMaximum(mBias),
                           pilTrnGetComment("DataMax"),
                           "ESO*", 1);

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("SummedExposureTime"),
                           0.,
                           pilTrnGetComment("SummedExposureTime"),
                           "ESO PRO*", 1);

    insertIntDescriptor(&(mBias->descs),
                        pilTrnGetKeyword("NFramesCombined"),
                        goodFrames,
                        pilTrnGetComment("NFramesCombined"),
                        "ESO PRO*", 1);

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("DataMedian"),
                           imageMedian(mBias),
                           pilTrnGetComment("DataMedian"),
                           "ESO PRO*", 1);

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("DataStdDeviation"),
                           imageSigma(mBias),
                           pilTrnGetComment("DataStdDeviation"),
                           "ESO PRO*", 1);

    insertDoubleDescriptor(&(mBias->descs),
                           pilTrnGetKeyword("DataMean"),
                           imageMean(mBias),
                           pilTrnGetComment("DataMean"),
                           "ESO PRO*", 1);

    /*
     * Create the product file on disk, set the product attributes and
     * update the set of frames.
     */

    if (createFitsImage(mbiasName, mBias, mbiasTag) == VM_FALSE) {
        cpl_msg_error(task, "Cannot create local product file %s!", mbiasName);

        deleteTable(ccdTable);
        deleteImage(mBias);

        return EXIT_FAILURE;
    }
    else {
        productFrame = newPilFrame(mbiasName, mbiasTag);

        pilFrmSetType(productFrame, PIL_FRAME_TYPE_PRODUCT);
        pilFrmSetFormat(productFrame, PIL_FRAME_FORMAT_IMAGE);
        pilFrmSetProductLevel(productFrame, PIL_PRODUCT_LEVEL_PRIMARY);
        pilFrmSetProductType(productFrame, PIL_PRODUCT_TYPE_REDUCED);

        pilSofInsert(sof, productFrame);
    }


    /*
     * Cleanup.
     */

    deleteTable(ccdTable);
    deleteImage(mBias);
 
    return EXIT_SUCCESS;

}


/*
 * Build table of contents, i.e. the list of available plugins, for
 * this module. This function is exported.
 */

cxint
cpl_plugin_get_info(cpl_pluginlist *list)
{

    cpl_recipe *recipe = cpl_calloc(1, sizeof *recipe);
    cpl_plugin *plugin = &recipe->interface;


    cpl_plugin_init(plugin,
                    CPL_PLUGIN_API,
                    VIMOS_BINARY_VERSION,
                    CPL_PLUGIN_TYPE_RECIPE,
                    "vmbias",

    "Create a master bias from set of raw bias frames.",

    "This recipe is used to create a master bias frame from a set of raw\n"
    "bias frames.\n\n" 
    "Input files:\n\n"
    "  DO category:  Type:       Explanation:     Required:\n"
    "  BIAS          Raw         Bias exposure       Y\n"
    "  CCD_TABLE     Calib       Bad pixel table     .\n\n"
    "Output files:\n\n"
    "  DO category:  Data type:  Explanation:\n"
    "  MASTER_BIAS   FITS image  Master bias\n\n"
    "A CCD table must be specified only if a bad pixel cleaning is\n"
    "requested.\n\n"
    "For more details, please refer to the VIMOS Pipeline User's Guide.",

    "ESO VIMOS Pipeline Team",

    PACKAGE_BUGREPORT,

    "This file is part of the VIMOS Instrument Pipeline\n"
    "Copyright (C) 2002-2005 European Southern Observatory\n\n"
    "This program is free software; you can redistribute it and/or modify\n"
    "it under the terms of the GNU General Public License as published by\n"
    "the Free Software Foundation; either version 2 of the License, or\n"
    "(at your option) any later version.\n\n"
    "This program is distributed in the hope that it will be useful,\n"
    "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
    "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
    "GNU General Public License for more details.\n\n"
    "You should have received a copy of the GNU General Public License\n"
    "along with this program; if not, write to the Free Software Foundation,\n"
    "Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n",

                    vmbias_create,
                    vmbias_exec,
                    vmbias_destroy);

    cpl_pluginlist_append(list, plugin);
    
    return 0;

}
