/* $Id: naco_spc_wavecal.c,v 1.76 2012-09-06 08:29:10 llundin Exp $
 *
 * This file is part of the NACO Pipeline
 * Copyright (C) 2002,2003 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  02111-1307  USA
 */

/*
 * $Author: llundin $
 * $Date: 2012-09-06 08:29:10 $
 * $Revision: 1.76 $
 * $Name: not supported by cvs2svn $
 */

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

/*-----------------------------------------------------------------------------
                                Includes
 -----------------------------------------------------------------------------*/

#include "naco_recipe.h"
#if 0
#include "irplib_distortion.h"
#endif

#include "irplib_wlxcorr.h"

#include "naco_spc.h"

#include "irplib_polynomial.h"
#include "irplib_wavecal.h"
#include "irplib_distortion.h"


#include <string.h>

/*-----------------------------------------------------------------------------
                            Recipe defines
 -----------------------------------------------------------------------------*/

#define RECIPE_STRING   "naco_spc_wavecal"

/*-----------------------------------------------------------------------------
                            Private Functions prototypes
 -----------------------------------------------------------------------------*/

static cpl_error_code naco_spc_wavecal_reduce(cpl_imagelist *,
                                              cpl_polynomial *,
                                              cpl_propertylist *,
                                              const char *,
                                              const irplib_framelist *,
                                              const cpl_table *,
                                              const cpl_bivector *,
                                              const cpl_parameterlist *);

static cpl_error_code naco_spc_wavecal_get_slitw(const cpl_propertylist *,
                                                 double *);


static cpl_error_code naco_spc_wavecal_1d(cpl_imagelist          * self,
                                          cpl_propertylist       * qclist,
                                          const cpl_image        * spec2d,
                                          const cpl_propertylist * plist,
                                          const char             * tag,
                                          const cpl_polynomial   * phdisp,
                                          const cpl_bivector     * argonlines,
                                          const cpl_parameterlist* parlist);

static
cpl_error_code naco_image_fill_column_from_dispersion(cpl_image *, int, cpl_boolean,
                                                      const cpl_polynomial *);

static cpl_error_code naco_spc_wavecal_distortion(cpl_image *,
                                                  cpl_propertylist *,
                                                  const cpl_parameterlist *);

static cpl_error_code naco_spc_physdisp_fill(cpl_polynomial *, const char *,
                                             const cpl_table *);

static cpl_error_code naco_spc_physdisp_transform(cpl_polynomial *, double,
                                                  double, int, double, double,
                                                  double, double);

static cpl_error_code naco_spc_wavecal_qc(cpl_propertylist *,
                                           cpl_propertylist *,
                                           const irplib_framelist *);

static cpl_error_code naco_spc_wavecal_save(cpl_frameset *,
                                             const cpl_parameterlist *,
                                             const cpl_propertylist *,
                                             const cpl_propertylist *,
                                             const cpl_imagelist *,
                                             const cpl_polynomial *,
                                             int, const irplib_framelist *);

static cpl_error_code naco_spc_wavecal_fill_table(cpl_table *,
                                                  const cpl_polynomial *);

static cpl_error_code naco_spc_wavecal_count_lines(cpl_propertylist     *,
                                                   const cpl_bivector   *,
                                                   double, double);
static
cpl_error_code naco_spc_wavecal_interpolate_rejected(cpl_image *,
                                                     const cpl_polynomial *);

static cpl_error_code naco_spc_wavecal_qc_lines(cpl_propertylist     *,
                                                const cpl_image *,
                                                const cpl_apertures *);

NACO_RECIPE_DEFINE(naco_spc_wavecal,
                   NACO_PARAM_PLOT | NACO_PARAM_FORCE,
                   "Wavelength calibration using arc lamps",
                   RECIPE_STRING
                   " -- NACO spectrocopy wavelength calibration from "
                   "lamp images.\n" 
                   "The files listed in the Set Of Frames (sof-file) "
                   "must be tagged:\n" 
                   "NACO-raw-file.fits " NACO_SPC_LAMPWAVE_RAW "\n"
                   "NACO-spectrum-model.fits " NACO_SPC_MODEL " .\n"
                   "\n"
                   NACO_SPC_MAN_MODESPLIT "\n\n"
                   "Furthermore, each input frame must have a value of "
                   NACO_PFITS_BOOL_LAMP1 " that is false for off-frames and "
                   "true for on-frames.\n"
                   "\n"
                   "Products:\n"
                   NACO_CALIB_ARC_MAP ": Primary HDU with the wavelength map, "
                   "i.e. the pixel values are wavelengths. The first extension "
                   "is a single-row table with the polynomial coefficients of "
                   "the 2D dispersion relation, lambda = P(x, y).\n"
                   NACO_CALIB_ARC_DIFF ": Primary HDU with the difference image "
                   "of the lamp-on and -off image. The first extension is the "
                   "distortion corrected image, were all the arc-lines are "
                   "straight. The dispersion in the distortion corrected image "
                   "is given by the central dispersion, lambda = P(512.5, y).");


/*----------------------------------------------------------------------------*/
/**
 * @defgroup naco_spc_wavecal   Wavelength calibration using arc lamps
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                                Functions code
 -----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/**
  @brief    The recipe data reduction part is implemented here 
  @param    framelist   the frames list
  @param    parlist     the parameters list
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static int naco_spc_wavecal(cpl_frameset            * framelist,
                             const cpl_parameterlist * parlist)
{
    cpl_errorstate       cleanstate = cpl_errorstate_get();
    irplib_framelist   * allframes = NULL;
    irplib_framelist   * rawframes = NULL;
    irplib_framelist   * f_one     = NULL;
    const char        ** taglist   = NULL;
    cpl_imagelist      * lamp_wave = cpl_imagelist_new();
    cpl_polynomial     * disp2d    = cpl_polynomial_new(2);
    cpl_propertylist   * qclist    = cpl_propertylist_new();
    cpl_propertylist   * paflist   = cpl_propertylist_new();
    const cpl_frame    * modelframe;
    const char         * modelfile;
    cpl_table          * modeltab  = NULL;
    const cpl_frame    * argonframe;
    const char         * argonfile;
    cpl_table          * argontab  = NULL;
    cpl_bivector       * argonlines= NULL;
    cpl_vector         * argonlinex= NULL;
    cpl_vector         * argonliney= NULL;
    int                  nargonlines;
    int                  nb_good   = 0;
    int                  nsets;
    int                  i;
    

    /* Identify the RAW and CALIB frames in the input frameset */
    skip_if (naco_dfs_set_groups(framelist));

    allframes = irplib_framelist_cast(framelist);
    skip_if(allframes == NULL);

    rawframes = irplib_framelist_extract(allframes, NACO_SPC_LAMPWAVE_RAW);
    skip_if(rawframes == NULL);

    irplib_framelist_empty(allframes);

    /* The parameters of the 1st guess of the dispersion relation */
    modelframe = cpl_frameset_find_const(framelist, NACO_SPC_MODEL);
    error_if (modelframe == NULL, CPL_ERROR_DATA_NOT_FOUND, "No input frame "
              "is tagged %s", NACO_SPC_MODEL);

    modelfile = cpl_frame_get_filename(modelframe);
    skip_if (modelfile == NULL);

    modeltab = cpl_table_load(modelfile, 1, 0);
    error_if (modeltab == NULL, cpl_error_get_code(), "Could not "
              "load the table with the model parameters");

    /* The argon lines */
    argonframe = cpl_frameset_find_const(framelist, NACO_SPC_ARGON);
    error_if (argonframe == NULL, CPL_ERROR_DATA_NOT_FOUND, "No input frame "
              "is tagged %s", NACO_SPC_ARGON);

    argonfile = cpl_frame_get_filename(argonframe);
    skip_if (argonfile == NULL);

    argontab = cpl_table_load(argonfile, 1, 0);
    error_if (argontab == NULL, cpl_error_get_code(), "Could not "
              "load the table with the argon lines");

    /* Wrap a bivector around the argontable */
    nargonlines = cpl_table_get_nrow(argontab);

    argonlinex = cpl_vector_wrap(nargonlines,
                                 cpl_table_get_data_double(argontab,
                                                           NACO_SPC_LAB_WAVE));
    skip_if(argonlinex == NULL);

    argonliney = cpl_vector_wrap(nargonlines,
                                 cpl_table_get_data_double(argontab,
                                                          NACO_SPC_LAB_INTENS));
    skip_if(argonliney == NULL);

#ifdef NACO_SPC_WAVECAL_LOG
    bug_if(cpl_vector_add_scalar(argonliney, 1.0));
    skip_if(cpl_vector_logarithm(argonliney, exp(1.0)));
#endif

    argonlines = cpl_bivector_wrap_vectors(argonlinex, argonliney);
    bug_if(argonlines == NULL);

    skip_if(irplib_framelist_load_propertylist_all(rawframes, 0, "^("
                                                   NACO_PFITS_REGEXP_SPCWAVE "|"
                                                   NACO_PFITS_REGEXP_SPCWAVE_PAF
                                                   ")$", CPL_FALSE));

    taglist = naco_framelist_set_tag(rawframes, naco_spc_make_tag, &nsets);
    skip_if(taglist == NULL);

    cpl_msg_info(cpl_func, "Identified %d setting(s) in %d frames",
                 nsets, irplib_framelist_get_size(rawframes));

    /* Extract settings and reduce each of them */
    for (i=0 ; i < nsets ; i++) {
        int n_one;
        cpl_error_code error;

        /* Reduce data set nb i */
        cpl_msg_info(cpl_func, "Reducing data set %d of %d", i+1, nsets);

        /* Reduce data set nb i */
        f_one = irplib_framelist_extract(rawframes, taglist[i]);

        bug_if (f_one == NULL);

        n_one = irplib_framelist_get_size(f_one);
        
        /* Reset the tag */
        bug_if(irplib_framelist_set_tag_all(f_one, NACO_SPC_LAMPWAVE_RAW));

        cpl_msg_info(cpl_func, "Reducing frame set %d of %d (size=%d) with "
                     "setting: %s", i+1, nsets, n_one, taglist[i]);

        error = naco_spc_wavecal_reduce(lamp_wave, disp2d, qclist, taglist[i],
                                        f_one, modeltab, argonlines, parlist);
        
        /* Save the products */
        if (error) {
            if (nsets > 1)
                irplib_error_recover(cleanstate, "Could not do the wavelength "
                                 "calibration for this setting");
        } else {
            cpl_errorstate prestate = cpl_errorstate_get();

            skip_if(naco_spc_wavecal_qc(qclist, paflist, f_one));

            /* PRO.CATG */
            bug_if (cpl_propertylist_append_string(paflist, CPL_DFS_PRO_CATG,
                                                   NACO_CALIB_ARC_MAP));

            /* modelframe and argonframe will not be modified */
            /* Cannot skip with frames shared among two framelists */
            (void)irplib_framelist_set(f_one, (cpl_frame*)modelframe, n_one);
            (void)irplib_framelist_set(f_one, (cpl_frame*)argonframe, n_one+1);
            (void)naco_spc_wavecal_save(framelist, parlist, qclist, paflist,
                                        lamp_wave, disp2d, i+1, f_one);
            (void)irplib_framelist_unset(f_one, n_one+1, NULL);
            (void)irplib_framelist_unset(f_one, n_one, NULL);
            skip_if(!cpl_errorstate_is_equal(prestate));

            do {
                cpl_image_delete(cpl_imagelist_unset(lamp_wave, 0));
            } while (cpl_imagelist_get_size(lamp_wave) > 0);

            nb_good++;
        }
        cpl_propertylist_empty(qclist);
        cpl_propertylist_empty(paflist);
        irplib_framelist_delete(f_one);
        f_one = NULL;
    }

    irplib_ensure(nb_good > 0, CPL_ERROR_DATA_NOT_FOUND,
                  "None of the %d sets could be reduced", nsets);
    
    end_skip;

    cpl_free(taglist);
    cpl_imagelist_delete(lamp_wave);
    cpl_table_delete(modeltab);
    cpl_bivector_unwrap_vectors(argonlines);
    (void)cpl_vector_unwrap(argonlinex);
    (void)cpl_vector_unwrap(argonliney);
    cpl_table_delete(argontab);
    irplib_framelist_delete(f_one);
    irplib_framelist_delete(allframes);
    irplib_framelist_delete(rawframes);
    cpl_propertylist_delete(qclist);
    cpl_propertylist_delete(paflist);
    cpl_polynomial_delete(disp2d);

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    The recipe data reduction part is implemented here 
  @param    imglist    The dispersion map and the combined image
  @param    disp2d     2D-dispersion relation
  @param    qclist     List of QC parameters
  @param    tag        The tag used for this setting
  @param    framelist  The list of frames
  @param    modeltab   The table of physical model parameters
  @param    argonlines The bivector of argon lines
  @param    parlist    The list of recipe parameters
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_reduce(cpl_imagelist * imglist,
                                              cpl_polynomial * disp2d,
                                              cpl_propertylist * qclist,
                                              const char * tag,
                                              const irplib_framelist * framelist,
                                              const cpl_table * modeltab,
                                              const cpl_bivector * argonlines,
                                              const cpl_parameterlist * parlist)
{
    cpl_image      * self = NULL;
    cpl_image      * corrected = NULL;
    cpl_imagelist  * difflist = cpl_imagelist_new();
    cpl_polynomial * disp1d = NULL;
    cpl_polynomial * collapse = cpl_polynomial_new(1);
    const int        nplot = naco_parameterlist_get_int(parlist, RECIPE_STRING,
                                                       NACO_PARAM_PLOT);
    const cpl_size   deg0 = 0;
    cpl_polynomial * phdisp = cpl_polynomial_new(1);
    const cpl_propertylist * plist
        = irplib_framelist_get_propertylist_const(framelist, 0);
    const char     * specmode
        = irplib_pfits_get_string(plist, NACO_PFITS_STRING_SPECMODE);
    const double     wlen
        = irplib_pfits_get_double(plist, NACO_PFITS_DOUBLE_CWLEN);
    cpl_propertylist * lampkeys = cpl_propertylist_new();
    cpl_stats      * stats = NULL;
    double           adumin, adumax;
    double           mse = 0.0;
    cpl_vector     * center = cpl_vector_new(2);
    int              ny; /* Wavelength resolution */

    bug_if (0);
    bug_if (imglist    == NULL);
    bug_if (qclist     == NULL);
    bug_if (framelist  == NULL);
    bug_if (modeltab   == NULL);
    bug_if (argonlines == NULL);
    bug_if (parlist    == NULL);
    bug_if (cpl_imagelist_get_size(imglist));

    /* On-frames have lamp1 on and lamp2 off */
    bug_if(cpl_propertylist_append_int(lampkeys, NACO_PFITS_BOOL_LAMP1, 1));
    bug_if(cpl_propertylist_append_int(lampkeys, NACO_PFITS_INT_LAMP2,  0));
  
    skip_if(naco_imagelist_load_diff(difflist, framelist, lampkeys));

    bug_if(0);

    if (cpl_imagelist_get_size(difflist) > 1) {
        cpl_msg_warning(cpl_func, "Averaging %d difference images "
                        "into one image for setting %s",
                        (int)cpl_imagelist_get_size(difflist), tag);
        self = cpl_imagelist_collapse_create(difflist);
    } else {
        self = cpl_imagelist_unset(difflist, 0);
    }

    stats = cpl_stats_new_from_image(self, CPL_STATS_MIN | CPL_STATS_MAX);
    adumin = cpl_stats_get_min(stats);
    adumax = cpl_stats_get_max(stats);

    cpl_msg_info(cpl_func, "Difference image for '%s' has ADUs in range: "
                 "%g -> %g", tag, adumin, adumax);
    if (adumin < 0.0) {
        cpl_msg_info(cpl_func, "Setting negative ADUs to zero");
        bug_if(cpl_image_threshold(self, 0.0, FLT_MAX, 0.0, FLT_MAX));
    }

    if (nplot > 2) {
      cpl_errorstate prestate = cpl_errorstate_get();
      cpl_plot_image("", "t 'Difference image'", "", self);
      if (!cpl_errorstate_is_equal(prestate)) {
        cpl_errorstate_set(prestate);
      }
    }

    corrected = cpl_image_duplicate(self);
    skip_if(naco_spc_wavecal_distortion(corrected, qclist, parlist));

    ny = cpl_image_get_size_y(self);
    bug_if (0);


    skip_if(naco_spc_physdisp_fill(phdisp, specmode, modeltab));

    cpl_msg_info(cpl_func, "Wavelength range using physical model [micron]: "
                 "%g -> %g",
                 cpl_polynomial_eval_1d(phdisp, 0.5,    NULL),
                 cpl_polynomial_eval_1d(phdisp, 0.5 + ny, NULL));

    if (naco_parameterlist_get_bool(parlist, RECIPE_STRING, NACO_PARAM_FORCE)) {
        /* FIXME: cpl_wlcalib_xc_best_poly() also fails with this */
        const cpl_size idegree = 0;
        const double dwlen
            = cpl_polynomial_eval_1d(phdisp, 0.5*(1+ny), NULL) - wlen;
        const double newval
            = cpl_polynomial_get_coeff(phdisp, &idegree) - dwlen;

        bug_if(cpl_polynomial_set_coeff(phdisp, &idegree, newval));
    }

    cpl_msg_info(cpl_func, "Central Wavelength (model <=> CWLEN) [micron]: "
                 "%g <=> %g", cpl_polynomial_eval_1d(phdisp, 0.5*(1+ny), NULL),
                 wlen);


    skip_if(naco_spc_wavecal_1d(imglist, qclist, self, plist, tag, phdisp,
                                argonlines, parlist));

    skip_if(irplib_polynomial_fit_2d_dispersion(disp2d,
                                                cpl_imagelist_get(imglist, 0),
                                                4, &mse));

    cpl_msg_info(cpl_func, "2D-dispersion with MSE=%g for setting %s", mse, tag);
    skip_if(cpl_polynomial_dump(disp2d, stdout));

    skip_if(naco_spc_wavecal_interpolate_rejected(cpl_imagelist_get(imglist, 0),
                                                  disp2d));

    bug_if(cpl_vector_set(center, 0, 0.5*(1+cpl_image_get_size_x(self))));
    bug_if(cpl_vector_set(center, 1, 0.5*(1+cpl_image_get_size_y(self))));

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC WLEN",
                                          cpl_polynomial_eval(disp2d, center)));

    /* Central 1D polynomial */
    bug_if(cpl_polynomial_set_coeff(collapse, &deg0,
                                    0.5 * (1+cpl_image_get_size_x(self))));

    disp1d = cpl_polynomial_extract(disp2d, 0, collapse);
    cpl_msg_info(cpl_func, "Central 1D-dispersion (at x=%g)",
                 0.5*(1+cpl_image_get_size_x(self)));
    skip_if(cpl_polynomial_dump(disp1d, stdout));

    end_skip;

    if (cpl_error_get_code()) {
        cpl_image_delete(self);
        cpl_image_delete(corrected);
    } else {
        cpl_imagelist_set(imglist, self, 1);
        cpl_imagelist_set(imglist, corrected, 2);
    }

    cpl_vector_delete(center);
    cpl_propertylist_delete(lampkeys);
    cpl_imagelist_delete(difflist);
    cpl_stats_delete(stats);
    cpl_polynomial_delete(phdisp);
    cpl_polynomial_delete(disp1d);
    cpl_polynomial_delete(collapse);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Perform the the 1D-wavelength calibration
  @param    self       The imagelist to hold the 2D-dispersion
  @param    qclist     List of QC parameters
  @param    spec2d     The corrected 2D-spectrum from the difference frame
  @param    plist      The propertylist from the on-frame
  @param    phdisp     The 1st guess of the dispersion relation
  @param    argonlines The lines to calibrate against
  @param    parlist    The list of recipe parameters
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_1d(cpl_imagelist          * self,
                                          cpl_propertylist       * qclist,
                                          const cpl_image        * spec2d,
                                          const cpl_propertylist * plist,
                                          const char             * tag,
                                          const cpl_polynomial   * phdisp,
                                          const cpl_bivector     * argonlines,
                                          const cpl_parameterlist* parlist)
{

    cpl_errorstate prestate = cpl_errorstate_get();
    const int        nx = cpl_image_get_size_x(spec2d); /* Spatial resolution */
    const int        ny = cpl_image_get_size_y(spec2d); /* Wavelength resolution */
    cpl_vector     * vspec1d  = NULL;
    cpl_polynomial * disp     = NULL;
    cpl_polynomial * dispcen  = NULL;
    cpl_polynomial * phshift  = cpl_polynomial_duplicate(phdisp);
    cpl_polynomial * dispdif  = cpl_polynomial_new(1);
    /* Initialize to zero */
    cpl_vector     * linepix
        = cpl_vector_wrap(cpl_bivector_get_size(argonlines),
                          cpl_calloc(cpl_bivector_get_size(argonlines),
                                     sizeof(double)));
    double           slitw = 0.0; /* Fix (false) uninit warning */
    const double     wfwhm = 4.0;
    double           xtrunc;
    double           xc, xcmean, xcstdev;
    const int        nplot = naco_parameterlist_get_int(parlist, RECIPE_STRING,
                                                        NACO_PARAM_PLOT);
    const int        plotstep = nplot > 0 ? nx / (1<<nplot) : nx + 1;
    const int        degree = cpl_polynomial_get_degree(phdisp);
    const int        fitdeg = degree > 4 ? degree : 4; /* Fit at least 4th degree */
    cpl_image      * imgdisp = cpl_image_new(nx, ny, cpl_image_get_type(spec2d));
    cpl_vector     * vxcall = cpl_vector_new(nx);
    /* One practical limitation on hshiftmax is that it may not be so big, as
       to cause phdisp to be evaluated outside its range of monotony. */
    const int        hshiftmax = ny/4;
    irplib_line_spectrum_model model;
    const double     pixstep = 0.25;
    const double     pixtol = 1e-5; /* 1e-6 leads to more accuracy, is slower */
    const int        istart = nx/2; /* Start on central column */
    const int        maxite = fitdeg * 100;
    int              ispec = istart;
    double           wl2dmin = FLT_MAX;
    double           wl2dmax = 0.0;
    cpl_boolean      isfirst = CPL_TRUE;

    bug_if(0);
    bug_if(qclist     == NULL);
    bug_if(spec2d     == NULL);
    bug_if(plist      == NULL);
    bug_if(phdisp     == NULL);
    bug_if(argonlines == NULL);


    skip_if(naco_spc_wavecal_get_slitw(plist, &slitw));

    cpl_msg_info(cpl_func, "Slitwidth [pixel]: %g", slitw);

    xtrunc = 0.5 * slitw + 5.0 * wfwhm * CPL_MATH_SIG_FWHM;

    memset(&model, 0, sizeof(model));
    model.wslit = slitw;
    model.wfwhm = wfwhm;
    model.xtrunc = xtrunc;
    model.lines = argonlines;
    model.linepix = linepix;
    model.cost = 0;

    vspec1d = cpl_vector_new_from_image_column(spec2d, ispec);
#ifdef NACO_SPC_WAVECAL_LOG
    bug_if(cpl_vector_add_scalar(vspec1d, 1.0));
    skip_if(cpl_vector_logarithm(vspec1d, exp(1.0)));
#endif

    if (nplot > 0) {
        char         * title = cpl_sprintf("t 'Uncalibrated 1D-spectrum "
                                           "using %s' w linespoints", tag);
        cpl_vector   * vphys = cpl_vector_new(ny);
        cpl_bivector * bspec1d = cpl_bivector_wrap_vectors(vphys, vspec1d);

        (void)cpl_vector_fill_polynomial(vphys, phdisp, 1.0, 1.0);


        cpl_plot_bivector("set grid;set xlabel 'Wavelength [micron]';"
                          "set ylabel 'Intensity [ADU]';", title, "",
                          bspec1d);
        cpl_free(title);
        cpl_vector_delete(vphys);
        cpl_bivector_unwrap_vectors(bspec1d);
        if (!cpl_errorstate_is_equal(prestate)) {
            cpl_errorstate_set(prestate);
        }
    }

    skip_if(irplib_polynomial_shift_1d_from_correlation(phshift, vspec1d,
                                                        (void*)&model,
                                                     irplib_vector_fill_line_spectrum,
                                                        hshiftmax, nplot > 0,
                                                        NULL));

    if (nplot > 0) {
        bug_if(irplib_plot_spectrum_and_model(vspec1d, phshift, (void*)&model,
                                            irplib_vector_fill_line_spectrum));
    }

    bug_if(cpl_polynomial_subtract(dispdif, phshift, phdisp));

    cpl_msg_info(cpl_func, "Changes to model polynomial by XC is of degree %d",
                 (int)cpl_polynomial_get_degree(dispdif));
    skip_if(cpl_polynomial_dump(dispdif, stdout));

    disp = cpl_polynomial_duplicate(phshift);
    /* In the unlikely event that the calibration fails on the central column */
    dispcen = cpl_polynomial_duplicate(disp); 

    /* Right half starting from central column */
    for (; ispec <= nx; ispec++) {
        const cpl_size prevcost = model.cost;

        if (!isfirst) {
            cpl_vector_delete(vspec1d);
            vspec1d = cpl_vector_new_from_image_column(spec2d, ispec);
#ifdef NACO_SPC_WAVECAL_LOG
            bug_if(cpl_vector_add_scalar(vspec1d, 1.0));
            skip_if(cpl_vector_logarithm(vspec1d, exp(1.0)));
#endif
        }

        xc = 0.0;
        if (irplib_polynomial_find_1d_from_correlation
            (disp, fitdeg, vspec1d, (void *)&model,
             irplib_vector_fill_line_spectrum, pixtol, pixstep, 2, maxite,
             &xc)) {
            irplib_error_recover(prestate, "Could not calibrate column %d of "
                                 "%d", ispec, nx);
            cpl_polynomial_copy(disp, dispcen);
            xc = 0.0;
        } else {
            double wl2d = cpl_polynomial_eval_1d(disp, 0.5, NULL);

            if (wl2d < wl2dmin) wl2dmin = wl2d;

            wl2d = cpl_polynomial_eval_1d(disp, ny + 0.5, NULL);
            if (wl2d > wl2dmax) wl2dmax = wl2d;

            if (ispec % plotstep == 0) {
                bug_if(irplib_plot_spectrum_and_model
                       (vspec1d, disp,
                        (void*)&model,
                        irplib_vector_fill_line_spectrum));
            }
#ifdef IRPLIB_SPC_DUMP
            /* Need irplib_wavecal.c rev. 1.12 through 1.15 */
            if (ispec == istart) {
                irplib_polynomial_tabulate(disp, vspec1d, (void *)&model,
                                           irplib_vector_fill_line_spectrum,
                                           50, 0.1);
            }
#endif
        }

        bug_if(cpl_vector_set(vxcall, ispec-1, xc));

        cpl_msg_info(cpl_func, "XC(%d): %g (cost=%u of %u)", ispec, xc,
                     (unsigned)(model.cost-prevcost), (unsigned)model.cost);

        bug_if(naco_image_fill_column_from_dispersion(imgdisp, ispec, xc <= 0.0,
                                                      disp));

        if (isfirst) {
            double cdisp;
            const double cwl = cpl_polynomial_eval_1d(disp, 0.5*ny + 0.5,
                                                      &cdisp);

            isfirst = CPL_FALSE;
            cpl_msg_info(cpl_func, "Center of setting %s has range %g -> %g "
                         "-> %g [um], center dispersion %g [nm/pixel] and "
                         "dispersion (of degree %d):", tag,
                         cpl_polynomial_eval_1d(disp, 0.5, NULL),
                         cwl,
                         cpl_polynomial_eval_1d(disp, ny + 0.5, NULL),
                         1e3*cdisp,
                         (int)cpl_polynomial_get_degree(disp));
            skip_if(cpl_polynomial_dump(disp, stdout));

            cpl_polynomial_copy(dispcen, disp);

            bug_if(cpl_polynomial_subtract(dispdif, disp, phshift));

            cpl_msg_info(cpl_func, "Changes to model polynomial by search is of"
                         " degree %d", (int)cpl_polynomial_get_degree(dispdif));
            skip_if(cpl_polynomial_dump(dispdif, stdout));

            bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISP XCORR",
                                                  xc));
        }
    }

    /* Left half, restarting from central column */
    cpl_polynomial_copy(disp, dispcen);

    for (ispec = istart-1; ispec > 0; ispec--) {
        const cpl_size prevcost = model.cost;

        cpl_vector_delete(vspec1d);
        vspec1d = cpl_vector_new_from_image_column(spec2d, ispec);
#ifdef NACO_SPC_WAVECAL_LOG
        bug_if(cpl_vector_add_scalar(vspec1d, 1.0));
        skip_if(cpl_vector_logarithm(vspec1d, exp(1.0)));
#endif

        xc = 0.0;
        if (irplib_polynomial_find_1d_from_correlation
            (disp, fitdeg, vspec1d, (void *)&model,
             irplib_vector_fill_line_spectrum, pixtol, pixstep, 2, maxite, &xc)) {
            if (ispec == 1 && wl2dmin >= wl2dmax) {
                error_if (0, cpl_error_get_code(),
                          "None of the columns could be calibrated");
            }

            irplib_error_recover(prestate, "Could not calibrate column %d of "
                                 "%d", ispec, nx);
            cpl_polynomial_copy(disp, dispcen);
        } else {
            double wl2d = cpl_polynomial_eval_1d(disp, 0.5, NULL);

            if (wl2d < wl2dmin) wl2dmin = wl2d;

            wl2d = cpl_polynomial_eval_1d(disp, ny + 0.5, NULL);
            if (wl2d > wl2dmax) wl2dmax = wl2d;

            if (ispec % plotstep == 0) {
                bug_if(irplib_plot_spectrum_and_model
                       (vspec1d, disp,
                        (void*)&model,
                        irplib_vector_fill_line_spectrum));
            }
        }

        bug_if(cpl_vector_set(vxcall, ispec-1, xc));

        cpl_msg_info(cpl_func, "XC(%d): %g (cost=%u of %u)", ispec, xc,
                     (unsigned)(model.cost-prevcost), (unsigned)model.cost);

        bug_if(naco_image_fill_column_from_dispersion(imgdisp, ispec, xc <= 0.0,
                                                      disp));
    }

    if (nplot > 0) {
        cpl_plot_vector("set grid;", "t 'XC over spatial range' w linespoints",
                        "", vxcall);
    }

    xcmean  = cpl_vector_get_mean(vxcall);
    xcstdev = cpl_vector_get_stdev(vxcall);

    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISP XCORR MEAN",
                                          xcmean));
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISP XCORR STDEV",
                                          xcstdev));

    cpl_msg_info(cpl_func,"Cross-correlation mean and stdev for setting %s: %g "
                 "%g", tag, xcmean, xcstdev);
    cpl_msg_info(cpl_func, "Total fitting cost: %u", (unsigned)model.cost);

    skip_if(naco_spc_wavecal_count_lines(qclist, argonlines, wl2dmin, wl2dmax));

    end_skip;

    if (cpl_error_get_code()) {
        cpl_image_delete(imgdisp);
    } else {
        cpl_imagelist_set(self, imgdisp, 0);
    }

    cpl_vector_delete(linepix);
    cpl_vector_delete(vxcall);
    cpl_polynomial_delete(dispdif);
    cpl_polynomial_delete(disp);
    cpl_polynomial_delete(dispcen);
    cpl_polynomial_delete(phshift);
    cpl_vector_delete(vspec1d);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Fill the 1D-polynomial with the coefficients of the physical model
  @param    self       The 1D-polynomial to fill
  @param    mode       The spectrum mode
  @param    modeltab   The table of physical model parameters
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_physdisp_fill(cpl_polynomial * self,
                                             const char * mode,
                                             const cpl_table * modeltab)
{

    int imode;
    const int nrows = cpl_table_get_nrow(modeltab);
    const char ** smode = cpl_table_get_data_string_const(modeltab,
                                                          NACO_SPC_LAB_MODE);

    bug_if(self     == NULL);
    bug_if(mode     == NULL);
    bug_if(modeltab == NULL);
    bug_if(cpl_polynomial_get_dimension(self) != 1);

    skip_if (smode == NULL);

    for (imode = 0; imode < nrows; imode++) {

        skip_if(smode[imode] == NULL);

        if (!strcmp(mode, smode[imode])) break;
    }

    error_if (imode == nrows, CPL_ERROR_UNSUPPORTED_MODE, "Unsupported value "
              "'%s' for " NACO_PFITS_STRING_SPECMODE, mode);

    cpl_msg_info(cpl_func, "Finding dispersion relation for spectrum mode "
                 "%d/%d: %s ", imode, nrows, mode);

    skip_if(naco_spc_physdisp_transform
            (self,
             cpl_table_get_double(modeltab, NACO_SPC_LAB_XMIN,  imode, NULL),
             cpl_table_get_double(modeltab, NACO_SPC_LAB_XMAX,  imode, NULL),
             cpl_table_get_int   (modeltab, NACO_SPC_LAB_ORDER, imode, NULL),
             cpl_table_get_double(modeltab, NACO_SPC_LAB_C1,    imode, NULL),
             cpl_table_get_double(modeltab, NACO_SPC_LAB_C2,    imode, NULL),
             cpl_table_get_double(modeltab, NACO_SPC_LAB_C3,    imode, NULL),
             cpl_table_get_double(modeltab, NACO_SPC_LAB_C4,    imode, NULL)));

#ifdef NACO_SPC_WAVECAL_S54_3_SH
    if (!strcmp("S54_3_SH", mode)) {
        double p0 = 1.36983;
        double p1 = 0.000165591;
        double p2 = 2.86676e-07;
        cpl_size degree = 0;

        bug_if(cpl_polynomial_set_coeff(self, &degree, p0));
        degree++;
        bug_if(cpl_polynomial_set_coeff(self, &degree, p1));
        degree++;
        bug_if(cpl_polynomial_set_coeff(self, &degree, p2));
        degree++;
        bug_if(cpl_polynomial_set_coeff(self, &degree, 0.0));
        degree++;
        bug_if(cpl_polynomial_set_coeff(self, &degree, 0.0));

        cpl_msg_warning(cpl_func, "Changing phdisp to simple fit:");
        skip_if(cpl_polynomial_dump(self, stdout));
    }
#endif

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Transform the Legendre polynomial to a polynomial in pixels
  @param    self       The 1D-polynomial to fill
  @param    xmin       The minimum pixel value used in the Legendre fit
  @param    xmax       The maximum pixel value used in the Legendre fit
  @param    fit_order  The order of the fit (number of coefficients)
  @param    c1         The 1st fitted coefficient of the Legendre polynomial
  @param    c2         The 2nd fitted coefficient of the Legendre polynomial
  @param    c3         The 3rd fitted coefficient of the Legendre polynomial
  @param    c4         The 4th fitted coefficient of the Legendre polynomial
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code
  @note ci is assumed to be in Aangstrom, the polynomial is in microns.

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_physdisp_transform(cpl_polynomial * self,
                                                  double xmin, double xmax,
                                                  int fit_order,
                                                  double c1, double c2,
                                                  double c3, double c4)
{

    const double alpha = 2.0/(xmax - xmin);
    const double beta  = -(xmax + xmin) / (xmax - xmin);
    double   value;
    double   lambdamin1, lambdamax1;
    double   lambdamin2, lambdamax2;
    cpl_size degree;

    bug_if(self == NULL);
    bug_if(cpl_polynomial_get_dimension(self) != 1);

    /* The specifics of the model may later become user definable */
    skip_if(xmin >= xmax);

    value = 1e-4 * (c1 - 0.5 * c3);
    degree = 0;
    bug_if(cpl_polynomial_set_coeff(self, &degree, value));

    value = 1e-4 * (c2 - 1.5 * c4);
    degree = 1;
    bug_if(cpl_polynomial_set_coeff(self, &degree, value));

    if (fit_order > 2) {
        value = 1e-4 * 1.5 * c3;
        degree = 2;
        bug_if(cpl_polynomial_set_coeff(self, &degree, value));

        if (fit_order > 3) {
            value = 1e-4 * 2.5 * c4;
            degree = 3;
            bug_if(cpl_polynomial_set_coeff(self, &degree, value));

            skip_if(fit_order > 4);
        }
    } else {
        skip_if(fit_order < 2);
    }


    lambdamin1 = cpl_polynomial_eval_1d(self, -1.0, NULL);
    lambdamax1 = cpl_polynomial_eval_1d(self,  1.0, NULL);


    /* Now transform the polynomial from the domain [-1;1] to [xmin;xmax],
       n = (2 * x - (xmax + xmin) / (xmax - xmin),
       n = x * alpha + beta */

    bug_if(cpl_polynomial_shift_1d(self, 0, beta));

    degree = 1;
    value = cpl_polynomial_get_coeff(self, &degree) * alpha;
    bug_if(cpl_polynomial_set_coeff(self, &degree, value));

    if (fit_order > 2) {
        degree = 2;
        value = cpl_polynomial_get_coeff(self, &degree) * alpha * alpha;
        bug_if(cpl_polynomial_set_coeff(self, &degree, value));

        if (fit_order > 3) {
            degree = 3;
            value = cpl_polynomial_get_coeff(self, &degree) * alpha * alpha
                * alpha;
            bug_if(cpl_polynomial_set_coeff(self, &degree, value));
        }
    }

    lambdamin2 = cpl_polynomial_eval_1d(self, xmin, NULL);
    lambdamax2 = cpl_polynomial_eval_1d(self, xmax, NULL);

    skip_if(cpl_polynomial_get_degree(self) != fit_order - 1);

    skip_if(cpl_polynomial_dump(self, stdout));

    cpl_msg_debug(cpl_func, "Interpolation minimum=%g: %g (%g)", xmin,
                 lambdamin1, lambdamin2-lambdamin1);
    cpl_msg_debug(cpl_func, "Interpolation maximum=%g: %g (%g)", xmax,
                 lambdamax1, lambdamax2-lambdamax1);

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Get the slitwidth in pixels
  @param    self      The propertylist with the slitname key
  @param    pslitw    The value or undefined on error
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code
  @note Currently the possible values are 'Slit_172mas' and 'Slit_86mas',
     with the width in mArcsecs.

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_get_slitw(const cpl_propertylist * self,
                                                 double * pslitw)
{
    const char * sslitw;
    int nvals;
    unsigned uslitw;
    double pixscale;


    bug_if(self   == NULL);
    bug_if(pslitw == NULL);

    sslitw = irplib_pfits_get_string(self, NACO_PFITS_STRING_SLITNAME);
    skip_if(sslitw == NULL);

    nvals = sscanf(sslitw, "Slit_%u", &uslitw);

    error_if(nvals != 1, CPL_ERROR_UNSUPPORTED_MODE, "Unsupported value of '"
             NACO_PFITS_STRING_SLITNAME ": %s", sslitw);

    pixscale = irplib_pfits_get_double(self, NACO_PFITS_DOUBLE_PIXSCALE);
    skip_if(0);
    error_if(pixscale <= 0.0, CPL_ERROR_ILLEGAL_INPUT, "Non-positive value of '"
             NACO_PFITS_DOUBLE_PIXSCALE ": %g", pixscale);

    *pslitw = (double)uslitw/(1000.0*pixscale); /* Convert from mas to pixel */

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Fill a column of the image with ts wavelengths
  @param  self   The image to modify
  @param  ispec  The column to modify
  @param  is_bad Flag the column as bad
  @param  disp   The 1D-dispersion relation
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
naco_image_fill_column_from_dispersion(cpl_image * self,
                                       int ispec, cpl_boolean is_bad,
                                       const cpl_polynomial * disp)
{

    const int ny = cpl_image_get_size_y(self);
    int i;

    bug_if(self == NULL);
    bug_if(disp == NULL);
    bug_if(cpl_polynomial_get_dimension(disp) != 1);
    bug_if(cpl_polynomial_get_degree(disp) < 1);

    for (i = 1; i <= ny; i++) {
        const double value = cpl_polynomial_eval_1d(disp, (double)i, NULL);
        cpl_image_set(self, ispec, i, value);
        if (is_bad) cpl_image_reject(self, ispec, i);
    }

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Correct the curvature of the lines in the 2D spectrum
  @param    self       The 2D-spectrum to correct
  @param    qclist     List of QC parameters
  @param    parlist    The list of recipe parameters
  @return   CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code naco_spc_wavecal_distortion(cpl_image * self,
                                           cpl_propertylist * qclist,
                                           const cpl_parameterlist * parlist)
{

    const int       fitdeg = 2;
    cpl_image     * copy = NULL;
    const int       nx = cpl_image_get_size_x(self);
    const int       ny = cpl_image_get_size_y(self);
    cpl_apertures * lines = NULL;
    cpl_polynomial* distortion = NULL;
    cpl_polynomial* yid2d = cpl_polynomial_new(2);
    cpl_vector    * profile = cpl_vector_new(CPL_KERNEL_DEF_SAMPLES);
    cpl_size        power[] = {0, 1};
    const int       nplot = naco_parameterlist_get_int(parlist, RECIPE_STRING,
                                                       NACO_PARAM_PLOT);
    cpl_polynomial* center = cpl_polynomial_new(1);
    cpl_polynomial* dist1d = NULL;
    cpl_vector    * dist1dfix = NULL;
    const cpl_size  i0 = 0;
    const cpl_size  i1 = 1;
    const double    xcent = 0.5*(nx + 1);
    const double    ycent = 0.5*(ny + 1);

    bug_if(0);
    bug_if(self    == NULL);
    bug_if(qclist  == NULL);
    bug_if(parlist == NULL);


    /* Distortion correction supports only vertical lines */
    bug_if(cpl_image_turn(self, 1));

    distortion = irplib_distortion_estimate(self, 1, 1, nx, ny,
                                            CPL_FALSE, 1e8,
                                            33,
                                            0.33, fitdeg, &lines);

    error_if(distortion == NULL, cpl_error_get_code(), "Curvature estimation "
             "failed");

    if (cpl_msg_get_level() <= CPL_MSG_DEBUG) {
        skip_if(cpl_polynomial_dump(distortion, stdout));
        cpl_apertures_dump(lines, stdout);
    }

    skip_if(naco_spc_wavecal_qc_lines(qclist, self, lines));

    /* Create the y-identity 2D-polynomial */
    bug_if(cpl_polynomial_set_coeff(yid2d, power, 1.0));
    
    /* Fill the kernel */
    bug_if(cpl_vector_fill_kernel_profile(profile, CPL_KERNEL_DEFAULT,
                                          CPL_KERNEL_DEF_WIDTH));

    /* Apply the distortion correction */
    copy = cpl_image_duplicate(self); /* Needed, so self can keep result */
    error_if (cpl_image_warp_polynomial(self, copy, distortion, yid2d, profile,
                                        CPL_KERNEL_DEF_WIDTH, profile,
                                        CPL_KERNEL_DEF_WIDTH),
              cpl_error_get_code(), "Distortion correction failed");

    /* Rotate distortion corrected image back */
    bug_if(cpl_image_turn(self, -1));

    if (nplot > 1) {
      cpl_errorstate prestate = cpl_errorstate_get();
      cpl_plot_image("", "t 'Distortion corrected image'", "", self);
      if (!cpl_errorstate_is_equal(prestate)) {
        cpl_errorstate_set(prestate);
      }

      /* Rotate distortion corrected image back */
      (void)cpl_image_turn(copy, -1);

      (void)cpl_image_subtract(copy, self);

      cpl_plot_image("", "t 'Distortion correction'", "", copy);
      if (!cpl_errorstate_is_equal(prestate)) {
        cpl_errorstate_set(prestate);
      }

    }

    skip_if(cpl_polynomial_dump(distortion, stdout));

    /* center is a zero-degree polynomial, p(x) = (nx+1)/2 */
    bug_if(cpl_polynomial_set_coeff(center, &i0, xcent));

    dist1d = cpl_polynomial_extract(distortion, 1, center);

    /* Reuse center-polynimial: P(y) = y */
    bug_if(cpl_polynomial_set_coeff(center, &i1, 1.0));
    bug_if(cpl_polynomial_set_coeff(center, &i0, 0.0));

    /* The deviation from the perfect center polynomial */
    bug_if(cpl_polynomial_subtract(center, dist1d, center));

    if (cpl_polynomial_get_degree(center) > 0) {
        const cpl_size ndist1d = cpl_polynomial_get_degree(dist1d);
        cpl_size       dist1dnreal;

        cpl_msg_info(cpl_func, "On the center column (x=%g) the distortion poly"
                     "nomial should be P(y)=y, its deviation from that has deg"
                     "ree %d:", xcent, (int)cpl_polynomial_get_degree(center));
        skip_if(cpl_polynomial_dump(center, stdout));

        if (ndist1d > 0) {
            cpl_errorstate prestate = cpl_errorstate_get();
            dist1dfix = cpl_vector_new(ndist1d);
            if (irplib_polynomial_solve_1d_all(dist1d, dist1dfix,
                                               &dist1dnreal)) {
            dist1dnreal = 0;
            irplib_error_recover(prestate, "Could not compute fix-points for "
                                 "%d-degree polynomial", (int)ndist1d);
            }
        } else {
            dist1dnreal = 0;
        }

        if (dist1dnreal > 0) {
            cpl_vector * dist1dfixreal = dist1dnreal == ndist1d ? dist1dfix
                : cpl_vector_wrap(dist1dnreal, cpl_vector_get_data(dist1dfix));
            cpl_msg_info(cpl_func, "The distortion correction has %d fix-"
                         "point(s) on the center column:", (int)dist1dnreal);
            cpl_vector_dump(dist1dfixreal, stdout);
            if (dist1dfixreal != dist1dfix)
                (void)cpl_vector_unwrap(dist1dfixreal);
        } else if (cpl_polynomial_get_coeff(dist1d, &i0) != 0.0) {
            /* Should not reach this point */
            cpl_msg_info(cpl_func, "The distortion correction has "
                         "no fix-points on the center column");
        } else {
            /* Should not reach this point */
            cpl_msg_info(cpl_func, "The distortion correction has "
                         "no fix-points on the center column");
        }

        cpl_msg_info(cpl_func, "The distortion correction moves the detector "
                     "center at (%g,%g) by (%g,%g)", xcent, ycent, 0.0,
                     cpl_polynomial_eval_1d(dist1d, ycent, NULL)-ycent);
    } else if (cpl_polynomial_get_coeff(center, &i0) != 0.0) {
        cpl_msg_info(cpl_func, "The distortion correction has no fix-points "
                     "on the center column, their Y-offset are [pixel]: %g",
                     cpl_polynomial_get_coeff(center, &i0));
    } else {
        cpl_msg_info(cpl_func, "The distortion correction has all points "
                     "on the center column (at x=%g) as fix-points", xcent);
    }


    power[0] = power[1] = 0;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DIST1",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));
    power[0] = 1;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISTY",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));
    power[0] = 2;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISTYY",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));

    power[0] = power[1] = 1;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISTXY",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));
    power[0] = 0;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISTX",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));
    power[1] = 2;
    bug_if(cpl_propertylist_append_double(qclist, "ESO QC DISTXX",
                                          cpl_polynomial_get_coeff(distortion,
                                                                   power)));

    end_skip;

    /* lines and distortion are rotated :-( */

    cpl_image_delete(copy);
    cpl_apertures_delete(lines);
    cpl_vector_delete(profile);
    cpl_vector_delete(dist1dfix);
    cpl_polynomial_delete(distortion);
    cpl_polynomial_delete(yid2d);
    cpl_polynomial_delete(center);
    cpl_polynomial_delete(dist1d);

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Append the QC parameters to the propertylist
  @param    qclist    List of QC parameters
  @param    paflist   List of PAF parameters
  @param    rawframes List of rawframes and their propertylists
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_qc(cpl_propertylist       * qclist,
                                          cpl_propertylist       * paflist,
                                          const irplib_framelist * rawframes)
{

    const cpl_propertylist * reflist
        = irplib_framelist_get_propertylist_const(rawframes, 0);
    const char pafcopy[] = "^(" NACO_PFITS_REGEXP_SPCWAVE_PAF ")$";


    bug_if (0);


    /* THE PAF FILE FOR QC PARAMETERS */
    skip_if (cpl_propertylist_copy_property_regexp(paflist, reflist, pafcopy,
                                                   0));
    skip_if (cpl_propertylist_append(paflist, qclist));

    bug_if (cpl_propertylist_copy_property_regexp(qclist, reflist, "^("
                                                 IRPLIB_PFITS_REGEXP_RECAL_LAMP
                                                  ")$", 0));
    end_skip;

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Save the wavecal recipe products on disk
  @param    set_tot   the total input frame set
  @param    parlist   the input list of parameters
  @param    qclist    List of QC parameters
  @param    paflist   List of PAF parameters
  @param    lamp_wave The dispersion map + combined image
  @param    set_nb    current setting number
  @param    rawframes the list of frames with the current settings
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_fill_table(cpl_table * self,
                                                  const cpl_polynomial * disp2d)
{

    const int degree = cpl_polynomial_get_degree(disp2d);
    const char * lunit = "micron";
    int i, j;

    bug_if (self   == NULL);
    bug_if (cpl_polynomial_get_dimension(disp2d) != 2);
    bug_if (degree < 1);

    bug_if(cpl_table_set_size(self, 1));

    for (i=0; i <= degree; i++) {
        for (j = 0; j <= i; j++) {
            const cpl_size powers[2] = {i-j, j};
            const double value = cpl_polynomial_get_coeff(disp2d, powers);
            char * label = cpl_sprintf("DISP2D_%d_%d", i-j, j);
            char * unit  = i > 1 ? cpl_sprintf("%s/pixel^%d", lunit, i)
                : cpl_sprintf(i ? "%s/pixel" : "%s", lunit);

            cpl_table_new_column(self, label, CPL_TYPE_DOUBLE);
            cpl_table_set_column_unit(self, label, unit);
            cpl_table_set_double(self, label, 0, value);

            cpl_free(label);
            cpl_free(unit);
            bug_if(0);
        }
    }

    end_skip;

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Save the wavecal recipe products on disk
  @param    set_tot   the total input frame set
  @param    parlist   the input list of parameters
  @param    qclist    List of QC parameters
  @param    paflist   List of PAF parameters
  @param    lamp_wave The dispersion map + combined image
  @param    disp2d    2D-dispersion relation
  @param    set_nb    current setting number
  @param    rawframes the list of frames with the current settings
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_save(cpl_frameset            * set_tot,
                                            const cpl_parameterlist * parlist,
                                            const cpl_propertylist  * qclist,
                                            const cpl_propertylist  * paflist,
                                            const cpl_imagelist     * lamp_wave,
                                            const cpl_polynomial    * disp2d,
                                            int                       set_nb,
                                            const irplib_framelist  * rawframes)
{
    cpl_frameset * proframes = irplib_frameset_cast(rawframes);
    cpl_table    * table2d = cpl_table_new(1);
    char         * filename  = NULL;
    cpl_propertylist * xtlist = cpl_propertylist_new();

    bug_if(cpl_propertylist_append_string(xtlist, "EXTNAME",
                                          "Wavelength calibration"));

    /* The wavelength map */
    filename = cpl_sprintf(RECIPE_STRING "_set%02d" CPL_DFS_FITS, set_nb);
    skip_if (irplib_dfs_save_image(set_tot, parlist, proframes,
                                cpl_imagelist_get_const(lamp_wave, 0),
                                CPL_BPP_IEEE_FLOAT, RECIPE_STRING,
                                NACO_CALIB_ARC_MAP, qclist, NULL,
                                naco_pipe_id, filename));

    bug_if(naco_spc_wavecal_fill_table(table2d, disp2d));

    skip_if(cpl_table_save(table2d, NULL, xtlist, filename, CPL_IO_EXTEND));

    /* The difference image with the arc exposure */
    cpl_free(filename);
    filename = cpl_sprintf(RECIPE_STRING "_set%02d_diff" CPL_DFS_FITS, set_nb);
    skip_if (irplib_dfs_save_image(set_tot, parlist, proframes,
                                cpl_imagelist_get_const(lamp_wave, 1),
                                CPL_BPP_IEEE_FLOAT, RECIPE_STRING,
                                NACO_CALIB_ARC_DIFF, qclist, NULL,
                                naco_pipe_id, filename));

    bug_if(cpl_propertylist_set_string(xtlist, "EXTNAME",
                                       "Difference Image"));
    skip_if (cpl_image_save(cpl_imagelist_get_const(lamp_wave, 2), filename,
                            CPL_BPP_IEEE_FLOAT, xtlist, CPL_IO_EXTEND));

#ifdef NACO_SAVE_PAF
    cpl_free(filename);
    filename = cpl_sprintf(RECIPE_STRING "_set%02d" CPL_DFS_PAF, set_nb);
    skip_if (cpl_dfs_save_paf("NACO", RECIPE_STRING, paflist, filename));
#else
    bug_if(paflist == NULL);
#endif

    end_skip;

    cpl_table_delete(table2d);
    cpl_free(filename);
    cpl_frameset_delete(proframes);
    cpl_propertylist_delete(xtlist);

    return cpl_error_get_code();

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Count the catalog lines used
  @param    self    List of QC parameters
  @param    lines   The bivector of lines
  @param    wlmin   The lower bound
  @param    wlmin   The upper bound
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_count_lines(cpl_propertylist   * self,
                                                   const cpl_bivector * lines,
                                                   double wlmin,
                                                   double wlmax)
{

    const cpl_vector * xlines  = cpl_bivector_get_x_const(lines);
    const double     * dxlines = cpl_vector_get_data_const(xlines);
    int minline, maxline;
    int clines;


    bug_if (self  == NULL);
    bug_if (lines == NULL);

    bug_if (wlmin < 0.0);
    bug_if (wlmax < wlmin);

    /* Find the 1st line */
    minline = cpl_vector_find(xlines, wlmin);

    /* The first line must be at least at wlmin */
    if (dxlines[minline] < wlmin) minline++;

    /* Find the last line */
    maxline = cpl_vector_find(xlines, wlmax);

    /* The last line must be at most at wlmax */
    if (dxlines[maxline] > wlmax) maxline--;

    clines = maxline >= minline ? maxline - minline : 0;

    bug_if(cpl_propertylist_append_int(self, "ESO QC DISP NUMCAT", clines));

    end_skip;

    return cpl_error_get_code();
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Use the 2D-dispersion relation to interpolate any bad pixels
  @param  self   The wavelength image map to modify
  @param  disp2d The 2D-dispersion relation
  @return CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
naco_spc_wavecal_interpolate_rejected(cpl_image * self,
                                      const cpl_polynomial * disp2d)
{

    const int nz = cpl_image_count_rejected(self);
    const int nx = cpl_image_get_size_x(self);
    const int ny = cpl_image_get_size_y(self);
    double power[2];
    cpl_vector * vpower = cpl_vector_wrap(2, power);
    int i, j;
    int k = nz;

    bug_if(self   == NULL);
    bug_if(disp2d == NULL);

    if (nz > 0) cpl_msg_info(cpl_func, "Interpolating %d poorly calibrated "
                             "pixels in the wavelength map", nz);

    for (i = 1; i <= nx && k > 0; i++) {
        for (j = 1; j <= ny && k > 0; j++) {
            if (cpl_image_is_rejected(self, i, j)) {
                power[0] = (double)i;
                power[1] = (double)j;
                cpl_image_set(self, i, j, cpl_polynomial_eval(disp2d, vpower));
                cpl_image_reject(self, i, j); /* Flagged as interpolated */
                k--;
            }
        }
    }

    end_skip;

    cpl_vector_unwrap(vpower);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief  Produce arc lines related QC params
  @param  self   The qclist to append to
  @param  spec2d The (straightened) 2D spectrum
  @param  lines  The aperture object with the lines
  @return CPL_ERROR_NONE on success, otherwise the relevant CPL error code

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code naco_spc_wavecal_qc_lines(cpl_propertylist    * self,
                                                const cpl_image     * spec2d,
                                                const cpl_apertures * lines)
{

    const int    nlines = cpl_apertures_get_size(lines);
    const int    ny     = cpl_image_get_size_y(spec2d);
    const double ycen   = 0.5 * (ny + 1);
    int          i, igood;
    char       * label  = NULL;
    cpl_vector * vmedian = cpl_vector_new(nlines);
    double       median;

    bug_if(self   == NULL);
    bug_if(spec2d == NULL);
    bug_if(lines  == NULL);

    bug_if(cpl_propertylist_append_int(self, "ESO QC ARCS NUM", nlines));

    igood = 0;
    for(i = 1; i <= nlines; i++) {
        cpl_errorstate prestate = cpl_errorstate_get();
        const double flux = cpl_apertures_get_flux(lines, i);
        const double xcen = cpl_apertures_get_centroid_x(lines, i);
        double fwhm_x, fwhm_y;

        if (cpl_image_get_fwhm(spec2d, xcen, ycen, &fwhm_x, &fwhm_y)) {
            irplib_error_recover(prestate, "Could not compute the FWHM for "
                                 "aperture %d of %d (with xcentroid=%g, flux=%g",
                                 i, nlines, xcen, flux);
            fwhm_x = -1.0;
        }

        if (fwhm_x > 0.0) {
            cpl_vector_set(vmedian, igood++, fwhm_x);
        }

        cpl_free(label);
        label = cpl_sprintf("ESO QC ARCS%d XPOS", i);
        cpl_propertylist_append_double(self, label, xcen);

        cpl_free(label);
        label = cpl_sprintf("ESO QC ARCS%d FWHM", i);

        cpl_propertylist_append_double(self, label, fwhm_x);

        cpl_free(label);
        label = cpl_sprintf("ESO QC ARCS%d FLUX", i);

        cpl_propertylist_append_double(self, label, flux);
    }
    bug_if(0);

    bug_if(cpl_propertylist_append_int(self, "ESO QC ARCS NUMGOOD", igood));

    if (igood > 0) {
        bug_if(cpl_vector_set_size(vmedian, igood));
        median = cpl_vector_get_median(vmedian);
    } else {
        median = -1.0;
    }

    bug_if(cpl_propertylist_append_double(self, "ESO QC FWHM MED", median));

    end_skip;

    cpl_vector_delete(vmedian);
    cpl_free(label);

    return cpl_error_get_code();
}
