/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2017 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#if HAVE_POPEN && HAVE_PCLOSE
#define _DEFAULT_SOURCE
#define _BSD_SOURCE /* force popen/pclose, mkdtemp definitions from stdio/stdlib */
#endif
#include <cpl.h>
#include <math.h>
#include <string.h>

#include "muse_wavecalib.h"
#include "muse_instrument.h"

#include "muse_combine.h"
#include "muse_cplwrappers.h"
#include "muse_data_format_z.h"
#include "muse_dfs.h"
#include "muse_tracing.h"
#include "muse_utils.h"

/*----------------------------------------------------------------------------*
 *                             Debugging Macros                               *
 *         Set these to 1 or higher for (lots of) debugging output            *
 *----------------------------------------------------------------------------*/
#define SEARCH_DEBUG 0 /* debugging in muse_wave_lines_search(), 1 or higher */
#define SEARCH_DEBUG_FILES 0 /* save different versions of the columns into  *
                              * FITS files in muse_wave_lines_search()       */
#define DEBUG_GAUSSFIT 0 /* debugging the Gaussian fit in muse_wave_lines_search() */
#define MUSE_WAVE_LINES_SEARCH_SHIFT_WARN 3.0 /* [pix] output debug message if    *
                                               * a shift of more than this occurs */
#define MUSE_WAVE_LINE_FIT_MAXSHIFT 2.0 /* [pix] don't use the fit when a shift *
                                         * of more than this is detected        */
#define MUSE_WAVE_LINE_HANDLE_SHIFT_LIMIT 0.25 /* [pix] maximum shift between *
                                                * adjacent CCD columns        */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_wavecalib    Wavelength calibration
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @brief   MUSE wavelength calibration arc line fit properties table definition.

  The wavelength calibration routines use a table to store the results of
  (Gaussian) fits to the arc lines. This table is not used anywhere else within
  the MUSE DRL. It has the following columns:

    - <tt>lampno</tt>: number of the lamp
    - <tt>lampname</tt>: name of the lamp
    - <tt>x</tt>: x-position on CCD
    - <tt>y</tt>: first-guess y-position on CCD
    - <tt>peak</tt>: Peak of line
    - <tt>center</tt>: Gaussian line center
    - <tt>cerr</tt>: error estimate of line center
    - <tt>sigma</tt>: Gaussian sigma
    - <tt>fwhm</tt>: Gaussian FWHM
    - <tt>flux</tt>: Gaussian area (flux)
    - <tt>bg</tt>: background level
    - <tt>mse</tt>: mean squared error
    - <tt>lambda</tt>: identified wavelength of the line

  Not all table columns are always used, e.g. lambda can be empty if the arc
  lines were not yet identified.
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_wavelines_def[] = {
  { "lampno", CPL_TYPE_INT, "", "%d", "Number of the lamp", CPL_TRUE },
  { "lampname", CPL_TYPE_STRING, "", "%s", "Name of the lamp", CPL_TRUE },
  { "x", CPL_TYPE_DOUBLE, "pix", "%.2f", "x-position on CCD", CPL_TRUE },
  { "y", CPL_TYPE_DOUBLE, "pix", "%.2f", "first-guess y-position on CCD", CPL_TRUE },
  { "peak", CPL_TYPE_DOUBLE, "pix", "%g", "Peak of line", CPL_TRUE },
  { "center", CPL_TYPE_DOUBLE, "pix", "%.4f", "Gaussian line center", CPL_TRUE },
  { "cerr", CPL_TYPE_DOUBLE, "pix", "%.4f", "error estimate of line center", CPL_TRUE },
  { "sigma", CPL_TYPE_DOUBLE, "pix", "%.3f", "Gaussian sigma", CPL_TRUE },
  { "fwhm", CPL_TYPE_DOUBLE, "pix", "%.3f", "Gaussian FWHM", CPL_TRUE },
  { "flux", CPL_TYPE_DOUBLE, "count", "%g", "Gaussian area (flux)", CPL_TRUE },
  { "bg", CPL_TYPE_DOUBLE, "count", "%.2f", "background level", CPL_TRUE },
  { "mse", CPL_TYPE_DOUBLE, "", "%e", "mean squared error", CPL_TRUE },
  { "lambda", CPL_TYPE_DOUBLE, "Angstrom", "%9.3f",
    "identified wavelength of the line", CPL_TRUE },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*----------------------------------------------------------------------------*/
/**
  @brief   MUSE wavelength calibration residuals table definition.
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_wavedebug_def[] = {
  { "slice", CPL_TYPE_INT, "", "%02d", "slice number", CPL_TRUE},
  { "iteration", CPL_TYPE_INT, "", "%d", "iteration", CPL_TRUE},
  { "x", CPL_TYPE_INT, "pix", "%04d", "x-position on CCD", CPL_TRUE},
  { "y", CPL_TYPE_FLOAT, "pix", "%8.3f", "y-position on CCD", CPL_TRUE},
  { "lambda", CPL_TYPE_FLOAT, "Angstrom", "%8.3f", "wavelength", CPL_TRUE},
  { "residual", CPL_TYPE_DOUBLE, "Angstrom", "%.4e", "residual at this point", CPL_TRUE},
  { "rejlimit", CPL_TYPE_DOUBLE, "Angstrom", "%.4e",
    "rejection limit for this iteration", CPL_TRUE},
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/* corresponds to muse_wave_weighting_type, *
 * keep in sync with those values!          */
const char *muse_wave_weighting_string[] = {
  "uniform",
  "centroid error",
  "per-line RMS scatter",
  "centroid error plus per-line RMS scatter"
};

/*----------------------------------------------------------------------------*/
/**
  @brief   Allocate a wavelength parameters structure and fill it with defaults.
  @param   aHeader   optional FITS header to set instrument mode
  @return  the newly allocated structure

  Allocate memory to store the pointers of the <tt>muse_wave_params</tt>
  structure. The structure is filled with defaults that are known to give
  reasonable results, pflags is set to CPL_FALSE, so that by default not
  residuals table is created.

  If aHeader was given, the mode component is set to the correct MUSE instrument
  mode. If it is NULL, mode remains zero, indicating WFM-NOAO-N.

  Use <tt>muse_wave_params_delete()</tt> to free the memory of the returned
  object after use.
 */
/*----------------------------------------------------------------------------*/
muse_wave_params *
muse_wave_params_new(cpl_propertylist *aHeader)
{
  muse_wave_params *p = cpl_malloc(sizeof(muse_wave_params));
  /* set defaults */
  p->xorder = 2;
  p->yorder = 6;
  p->detsigma = 1.;
  p->ddisp = 0.05; /* from 1.20...1.30 Angstrom/pixel */
  p->tolerance = 0.1;
  p->linesigma = -1.;
  p->rflag = CPL_FALSE; /* do not create a residuals table by default */
  p->residuals = NULL;
  p->fitsigma = -1.;
  p->fitweighting = MUSE_WAVE_WEIGHTING_UNIFORM;
  p->targetrms = 0.03;
  if (aHeader) { /* otherwise p->mode stays zero which means WFM-NOAO-N */
    p->mode = muse_pfits_get_mode(aHeader);
    cpl_msg_debug(__func__, "Set mode %s (%d)", muse_pfits_get_insmode(aHeader),
                  p->mode);
  }
  return p;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Deallocate memory associated to a wavelength parameters structure.
  @param   aParams   arc line fit properties table

  Deallocates the <tt>residuals</tt> component of aParams before freeing the
  whole structure.
  As a safeguard, it checks if a valid pointer was passed, so that crashes
  cannot occur.
 */
/*----------------------------------------------------------------------------*/
void
muse_wave_params_delete(muse_wave_params *aParams)
{
  if (!aParams) {
    return;
  }
  cpl_table_delete(aParams->residuals);
  aParams->residuals = NULL;
  memset(aParams, 0, sizeof(muse_wave_params)); /* null out everything */
  cpl_free(aParams);
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Write wavecal related QC1 keywords about peaks into header.
  @param   aLines    arc line fit properties table
  @param   aHeader   the list of FITS headers to modify
  @param   aSlice    slice number to use for keyword generation

  If aSlice is 20 (this slice that is near the middle of a stack on the sky, so
  should never be affected by vignetting), then try to add per-lamp peak
  keywords as well.
 */
/*----------------------------------------------------------------------------*/
static void
muse_wave_calib_qc_peaks(cpl_table *aLines, cpl_propertylist *aHeader,
                         const unsigned short aSlice)
{
  char keyword[KEYWORD_LENGTH];
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_PEAK_MEAN, aSlice);
  cpl_propertylist_append_float(aHeader, keyword,
                                cpl_table_get_column_mean(aLines, "peak"));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_PEAK_STDEV, aSlice);
  cpl_propertylist_append_float(aHeader, keyword,
                                cpl_table_get_column_stdev(aLines, "peak"));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_PEAK_MIN, aSlice);
  cpl_propertylist_append_float(aHeader, keyword,
                                cpl_table_get_column_min(aLines, "peak"));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_PEAK_MAX, aSlice);
  cpl_propertylist_append_float(aHeader, keyword,
                                cpl_table_get_column_max(aLines, "peak"));
  if (aSlice != 20) {
    return;
  }
  /* if we are in slice 20 (a slice that is near the middle of a stack, *
   * so should never be vignetted, loop through all possible lamps      */
  int n, nlamps = muse_pfits_get_lampnum(aHeader);
  for (n = 1; n < nlamps; n++) {
    /* select and extract all table entries matching the lamp */
    cpl_table_unselect_all(aLines);
    cpl_table_or_selected_int(aLines, "lampno", CPL_EQUAL_TO, n);
    cpl_table *lamplines = cpl_table_extract_selected(aLines);
    if (cpl_table_get_nrow(lamplines) < 1) {
      cpl_table_delete(lamplines);
      continue;
    }
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LAMPl_LINES_PEAK_MEAN,
             aSlice, n);
    cpl_propertylist_append_float(aHeader, keyword,
                                  cpl_table_get_column_mean(lamplines, "peak"));
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LAMPl_LINES_PEAK_STDEV,
             aSlice, n);
    cpl_propertylist_append_float(aHeader, keyword,
                                  cpl_table_get_column_stdev(lamplines, "peak"));
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LAMPl_LINES_PEAK_MAX,
             aSlice, n);
    cpl_propertylist_append_float(aHeader, keyword,
                                  cpl_table_get_column_max(lamplines, "peak"));
    cpl_table_delete(lamplines);
  } /* for n (all lamps) */
} /* muse_wave_calib_qc_peaks() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Write some wavecal related QC1 keywords into FITS headers.
  @param   aLines    arc line fit properties table
  @param   aHeader   the list of FITS headers to modify
  @param   aRefLam   list of arc line wavelengths to be used as FWHM references
  @param   aSlice    slice number to use for keyword generation
 */
/*----------------------------------------------------------------------------*/
static void
muse_wave_calib_qc_fwhm_old(cpl_table *aLines, cpl_propertylist *aHeader,
                            cpl_vector *aRefLam, const unsigned short aSlice)
{
  int nlines = cpl_table_get_nrow(aLines);

  /* track the minimum and maximum resolutions and their wavelengths */
  double Rmin = DBL_MAX, Rmax = -DBL_MAX, wlmin = DBL_MAX, wlmax = -DBL_MAX;

  /* convert the properties of the relevant lines from the respective *
   * table columns into vectors for easy statistics computation       */
  cpl_vector *fwhms = cpl_vector_new(nlines),
             *resol = cpl_vector_new(cpl_vector_get_size(aRefLam));
  int i, iresol = 0;
  for (i = 0; i < nlines; i++) {
     /* FWHM is directly accessible, but needs to be converted to Angstrom */
    cpl_errorstate prestate = cpl_errorstate_get();
    double fwhm = cpl_table_get(aLines, "fwhm", i, NULL), /* in [pix] */
           sampling = kMuseSpectralSamplingA, /* sensible default in [A/pix] */
           lambda = cpl_table_get(aLines, "lambda", i, NULL),
           s1 = (lambda - cpl_table_get(aLines, "lambda", i - 1, NULL))
              / (cpl_table_get(aLines, "center", i, NULL)
                 - cpl_table_get(aLines, "center", i - 1, NULL)),
           s2 = (cpl_table_get(aLines, "lambda", i + 1, NULL)
                 - cpl_table_get(aLines, "lambda", i, NULL))
              / (cpl_table_get(aLines, "center", i + 1, NULL)
                 - cpl_table_get(aLines, "center", i, NULL));
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_errorstate_set(prestate); /* reset "Access beyond boundaries" errors */
    }
    if (i == 0) { /* no lower one */
      sampling = s2;
    } else if (i == nlines - 1) { /* no higher one */
      sampling = s1;
    } else {
      sampling = (s1 + s2) / 2.;
    }
    fwhm *= sampling; /* now in [A] */
    cpl_vector_set(fwhms, i, fwhm);
    /* spectral resolution R for this line R = lambda / dlambda */
    double R = lambda / fwhm;
    /* compare wavelength to those lines in the FWHM reference list */
    if (fabs(cpl_vector_get(aRefLam, cpl_vector_find(aRefLam, lambda)) - lambda)
        < FLT_EPSILON) {
      if (R < Rmin) {
        Rmin = R;
        wlmin = lambda;
      }
      if (R > Rmax) {
        Rmax = R;
        wlmax = lambda;
      }
      cpl_vector_set(resol, iresol++, R);
    }
  } /* for i (all arc lines) */
  cpl_vector_set_size(resol, iresol);

  char keyword[KEYWORD_LENGTH];
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MEAN, aSlice);
  cpl_propertylist_append_float(aHeader, keyword, cpl_vector_get_mean(fwhms));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_STDEV, aSlice);
  cpl_propertylist_append_float(aHeader, keyword, cpl_vector_get_stdev(fwhms));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MIN, aSlice);
  cpl_propertylist_append_float(aHeader, keyword, cpl_vector_get_min(fwhms));
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MAX, aSlice);
  cpl_propertylist_append_float(aHeader, keyword, cpl_vector_get_max(fwhms));
  cpl_vector_delete(fwhms);

  cpl_msg_debug(__func__, "Average spectral resolution in IFU %hhu is R=%4.0f, "
                "ranging from %4.0f (at %6.1fA) to %4.0f (at %6.1fA)",
                muse_utils_get_ifu(aHeader), cpl_vector_get_mean(resol),
                Rmin, wlmin, Rmax, wlmax);
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_RESOL, aSlice);
  cpl_propertylist_append_float(aHeader, keyword, cpl_vector_get_mean(resol));
  cpl_vector_delete(resol);
} /* muse_wave_calib_qc_fwhm_old() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute QC and output statistics of FWHM and spectral resolution R.
  @param   aFWHM     arc line fit properties table of all FWHM reference lines
  @param   aHeader   the list of FITS headers to modify
  @param   aSlice    slice number to use for keyword generation
 */
/*----------------------------------------------------------------------------*/
static void
muse_wave_calib_qc_fwhm(cpl_table *aFWHM, cpl_propertylist *aHeader,
                        const unsigned short aSlice)
{
  /* compute the spectral resolution at each wavelength, as *
   *    R = lambda / fwhm                                   *
   * where both lambda and fwhm are [Angstrom]              */
  cpl_table_duplicate_column(aFWHM, "R", aFWHM, "lambda");
  cpl_table_divide_columns(aFWHM, "R", "fwhm");
  cpl_table_set_column_unit(aFWHM, "R", "");

#if 0 /* AIT Gaussian FWHM */
  /* range < 600 nm (as in PR5) */
  cpl_table_unselect_all(aFWHM);
  cpl_table_or_selected_double(aFWHM, "lambda", CPL_LESS_THAN, 6000.);
  cpl_table *tblue = cpl_table_extract_selected(aFWHM);
  /* range > 800 nm (as in PR5) */
  cpl_table_unselect_all(aFWHM);
  cpl_table_or_selected_double(aFWHM, "lambda", CPL_GREATER_THAN, 8000.);
  cpl_table *tred = cpl_table_extract_selected(aFWHM);
  /* range 600 - 800 nm (as in PR5) */
  cpl_table_unselect_all(aFWHM);
  cpl_table_or_selected_double(aFWHM, "lambda", CPL_NOT_LESS_THAN, 6000.);
  cpl_table_and_selected_double(aFWHM, "lambda", CPL_NOT_GREATER_THAN, 8000.);
  cpl_table *tgreen = cpl_table_extract_selected(aFWHM);
#if 0
  printf("all:\n");
  cpl_table_dump(aFWHM, 0, 100000, stdout);
  fflush(stdout);
  printf("blue:\n");
  cpl_table_dump(tblue, 0, 100000, stdout);
  fflush(stdout);
  printf("green:\n");
  cpl_table_dump(tgreen, 0, 100000, stdout);
  fflush(stdout);
  printf("red:\n");
  cpl_table_dump(tred, 0, 100000, stdout);
  fflush(stdout);
#endif
#endif /* AIT Gaussian FWHM */

  /* fill the QC parameters */
  char keyword[KEYWORD_LENGTH];
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_RESOL, aSlice);
  double rmean = cpl_table_get_column_mean(aFWHM, "R");
  cpl_propertylist_update_float(aHeader, keyword, rmean);

  double fmean = cpl_table_get_column_mean(aFWHM, "fwhm"),
         fstdev = cpl_table_get_column_stdev(aFWHM, "fwhm"),
         flo = cpl_table_get_column_min(aFWHM, "fwhm"),
         fhi = cpl_table_get_column_max(aFWHM, "fwhm");
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MEAN, aSlice);
  cpl_propertylist_update_float(aHeader, keyword, fmean);
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_STDEV, aSlice);
  cpl_propertylist_update_float(aHeader, keyword, fstdev);
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MIN, aSlice);
  cpl_propertylist_update_float(aHeader, keyword, flo);
  snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_FWHM_MAX, aSlice);
  cpl_propertylist_update_float(aHeader, keyword, fhi);

#if 0 /* AIT Gaussian FWHM */
  /* output all the stuff as debug messages */
  cpl_msg_debug(__func__, "Gaussian FWHM [%s]:\n"
                "\tFWHM all   (%d values)\t%.3f +/- %.3f (%.3f) %.3f...%.3f\n"
                "\tFWHM blue  (%d values)\t%.3f +/- %.3f (%.3f)\n"
                "\tFWHM green (%d values)\t%.3f +/- %.3f (%.3f)\n"
                "\tFWHM red   (%d values)\t%.3f +/- %.3f (%.3f)",
                cpl_table_get_column_unit(aFWHM, "fwhm"),
                (int)cpl_table_get_nrow(aFWHM), fmean, fstdev, flo, fhi,
                cpl_table_get_column_median(aFWHM, "fwhm"),
                (int)cpl_table_get_nrow(tblue),
                cpl_table_get_column_mean(tblue, "fwhm"),
                cpl_table_get_column_stdev(tblue, "fwhm"),
                cpl_table_get_column_median(tblue, "fwhm"),
                (int)cpl_table_get_nrow(tgreen),
                cpl_table_get_column_mean(tgreen, "fwhm"),
                cpl_table_get_column_stdev(tgreen, "fwhm"),
                cpl_table_get_column_median(tgreen, "fwhm"),
                (int)cpl_table_get_nrow(tred),
                cpl_table_get_column_mean(tred, "fwhm"),
                cpl_table_get_column_stdev(tred, "fwhm"),
                cpl_table_get_column_median(tred, "fwhm"));
  cpl_msg_debug(__func__, "Gaussian spectral resolution R:\n"
                "\tR all   (%d values)\t%.1f +/- %.1f (%.1f) %.1f...%.1f\n"
                "\tR blue  (%d values)\t%.1f +/- %.1f (%.1f)\n"
                "\tR green (%d values)\t%.1f +/- %.1f (%.1f)\n"
                "\tR red   (%d values)\t%.1f +/- %.1f (%.1f)",
                (int)cpl_table_get_nrow(aFWHM), rmean,
                cpl_table_get_column_stdev(aFWHM, "R"),
                cpl_table_get_column_median(aFWHM, "R"),
                cpl_table_get_column_min(aFWHM, "R"),
                cpl_table_get_column_max(aFWHM, "R"),
                (int)cpl_table_get_nrow(tblue),
                cpl_table_get_column_mean(tblue, "R"),
                cpl_table_get_column_stdev(tblue, "R"),
                cpl_table_get_column_median(tblue, "R"),
                (int)cpl_table_get_nrow(tgreen),
                cpl_table_get_column_mean(tgreen, "R"),
                cpl_table_get_column_stdev(tgreen, "R"),
                cpl_table_get_column_median(tgreen, "R"),
                (int)cpl_table_get_nrow(tred),
                cpl_table_get_column_mean(tred, "R"),
                cpl_table_get_column_stdev(tred, "R"),
                cpl_table_get_column_median(tred, "R"));
  cpl_table_delete(tblue);
  cpl_table_delete(tgreen);
  cpl_table_delete(tred);
#endif /* AIT Gaussian FWHM */
  cpl_table_erase_column(aFWHM, "R");
} /* muse_wave_calib_qc_fwhm() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute QC parameters about wavelength locations.
  @param   aImage    the muse image of to get the vertical size and the 
  @param   aSlice    slice number to use for keyword generation
  @param   aTrace    array of the three tracing polynomials
  @param   aFit      two-dimensional polynomial wavelength solution

  The fixed evaluation point for QC_WAVECAL_SLICEj_WLPOS and
  QC_WAVECAL_SLICEj_WLEN is the approximate slice center.
 */
/*----------------------------------------------------------------------------*/
static void
muse_wave_calib_qc_lambda(muse_image *aImage, const unsigned short aSlice,
                          cpl_polynomial **aTrace, cpl_polynomial *aFit)
{
  int ny = cpl_image_get_size_y(aImage->data);
  unsigned char ifu = muse_utils_get_ifu(aImage->header);

  /* wavelength differences at bottom and top of each slice as QC parameters */
  double xbotl = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_LEFT], 1, NULL),
         xbotr = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_RIGHT], 1, NULL),
         xtopl = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_LEFT], ny, NULL),
         xtopr = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_RIGHT], ny, NULL);
  cpl_vector *pos = cpl_vector_new(2);
  cpl_vector_set(pos, 0, xbotl);
  cpl_vector_set(pos, 1, 1);
  double wlbotl = cpl_polynomial_eval(aFit, pos);
  cpl_vector_set(pos, 0, xbotr);
  cpl_vector_set(pos, 1, 1);
  double wlbotr = cpl_polynomial_eval(aFit, pos);
  cpl_vector_set(pos, 0, xtopl);
  cpl_vector_set(pos, 1, ny);
  double wltopl = cpl_polynomial_eval(aFit, pos);
  cpl_vector_set(pos, 0, xtopr);
  cpl_vector_set(pos, 1, ny);
  double wltopr = cpl_polynomial_eval(aFit, pos);
#if 0
  cpl_msg_debug(__func__, "Wavelengths at slice corners in slice %hu of IFU %hhu: "
                "botl(%.2f,1)=%f, botr(%.2f,1)=%f, topl(%.2f,%d)=%f, topr(%.2f,%d)"
                "=%f", aSlice, ifu, xbotl, wlbotl, xbotr, wlbotr,
                xtopl, ny, wltopl, xtopr, ny, wltopr);
#endif
  char *keyword = cpl_sprintf(QC_WAVECAL_SLICEj_DWLEN_BOT, aSlice);
  cpl_propertylist_append_float(aImage->header, keyword, wlbotl - wlbotr);
  cpl_free(keyword);
  keyword = cpl_sprintf(QC_WAVECAL_SLICEj_DWLEN_TOP, aSlice);
  cpl_propertylist_append_float(aImage->header, keyword, wltopl - wltopr);
  cpl_free(keyword);

#define DWLEN_WARN_LIMIT_N 6.5 /* 5.7 Angstrom differences can happen... */
#define DWLEN_WARN_LIMIT_E 5.9 /* ... at the bottom in both modes        */
  double limit = DWLEN_WARN_LIMIT_E;
  cpl_errorstate prestate = cpl_errorstate_get();
  if (muse_pfits_get_mode(aImage->header) != MUSE_MODE_WFM_NONAO_E &&
      muse_pfits_get_mode(aImage->header) != MUSE_MODE_WFM_AO_E) {
    limit = DWLEN_WARN_LIMIT_N;
  }
  cpl_errorstate_set(prestate); /* swallow possible error about missing INS MODE */
  if (fabs(wlbotl - wlbotr) > limit) {
    cpl_msg_warning(__func__, "Wavelength differences at bottom of slice %hu "
                    "of IFU %hhu are large (%f Angstrom)!", aSlice, ifu,
                    wlbotl - wlbotr);
  }
  if (fabs(wltopl - wltopr) > DWLEN_WARN_LIMIT_E) { /* strict limit at red end */
    cpl_msg_warning(__func__, "Wavelength differences at top of slice %hu of IFU "
                    "%hhu are large (%f Angstrom)!", aSlice, ifu, wltopl - wltopr);
  }

  /* generate some arbitrary evaluation point, approx. at *
   * the slice center, and compute the wavelength there.  */
  const double yc = (1. + ny) / 2.,
               xc = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_CENTER], yc, NULL);
  cpl_vector_set(pos, 0, xc);
  cpl_vector_set(pos, 1, yc);
  keyword = cpl_sprintf(QC_WAVECAL_SLICEj_WLPOS, aSlice);
  cpl_propertylist_append_float(aImage->header, keyword, yc);
  cpl_free(keyword);
  keyword = cpl_sprintf(QC_WAVECAL_SLICEj_WLEN, aSlice);
  cpl_propertylist_append_float(aImage->header, keyword,
                                cpl_polynomial_eval(aFit, pos));
  cpl_free(keyword);
  cpl_vector_delete(pos);
} /* muse_wave_calib_qc_lambda() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Produce some useful output in the form of a wavelength calibration
           summary.
  @param   aHeader   the list of FITS headers to read from
  @param   aWave     final wavelength calibration table

  Loop over all slices to find average (or median, min, max, stdev) values of
  the QC parameters.

  @error{output warning and return, aWave is NULL}
 */
/*----------------------------------------------------------------------------*/
static void
muse_wave_calib_output_summary(const cpl_propertylist *aHeader,
                               const cpl_table *aWave)
{
  unsigned char ifu = muse_utils_get_ifu(aHeader);
  if (!aWave) {
    cpl_msg_warning(__func__, "Wavelength solution missing for IFU %hhu, no "
                    "summary!", ifu);
    return;
  }
  cpl_vector *found = cpl_vector_new(kMuseSlicesPerCCD),
             *ident = cpl_vector_new(kMuseSlicesPerCCD),
             *used = cpl_vector_new(kMuseSlicesPerCCD),
             *resolution = cpl_vector_new(kMuseSlicesPerCCD);
  unsigned short islice;
  for (islice = 0; islice < kMuseSlicesPerCCD; islice++) {
    char keyword[KEYWORD_LENGTH];
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_NDET,
             (unsigned short)(islice + 1));
    cpl_vector_set(found, islice, cpl_propertylist_get_int(aHeader, keyword));
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_LINES_NID,
             (unsigned short)(islice + 1));
    cpl_vector_set(ident, islice, cpl_propertylist_get_int(aHeader, keyword));
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_FIT_NLINES,
             (unsigned short)(islice + 1));
    cpl_vector_set(used, islice, cpl_propertylist_get_int(aHeader, keyword));
    snprintf(keyword, KEYWORD_LENGTH, QC_WAVECAL_SLICEj_RESOL,
             (unsigned short)(islice + 1));
    cpl_vector_set(resolution, islice, cpl_propertylist_get_float(aHeader, keyword));
  } /* for islice (all kMuseSlicesPerCCD) */
  cpl_msg_info(__func__, "Summary of wavelength solution for IFU %hhu:\n"
               "\tDetections per slice:        %d ... %d\n"
               "\tIdentified lines per slice:  %d ... %d\n"
               "\tLines per slice used in fit: %d ... %d\n"
               "\tRMS of fit [Angstrom]:       %5.3f +/- %5.3f (%5.3f ... %5.3f)\n"
               "\tMean spectral resolution R:  %6.1f +/- %5.1f", ifu,
               (int)cpl_vector_get_min(found), (int)cpl_vector_get_max(found),
               (int)cpl_vector_get_min(ident), (int)cpl_vector_get_max(ident),
               (int)cpl_vector_get_min(used), (int)cpl_vector_get_max(used),
               sqrt(cpl_table_get_column_mean(aWave, MUSE_WAVECAL_TABLE_COL_MSE)),
               sqrt(cpl_table_get_column_stdev(aWave, MUSE_WAVECAL_TABLE_COL_MSE)),
               sqrt(cpl_table_get_column_min(aWave, MUSE_WAVECAL_TABLE_COL_MSE)),
               sqrt(cpl_table_get_column_max(aWave, MUSE_WAVECAL_TABLE_COL_MSE)),
               cpl_vector_get_mean(resolution), cpl_vector_get_stdev(resolution));
  cpl_vector_delete(found);
  cpl_vector_delete(ident);
  cpl_vector_delete(used);
  cpl_vector_delete(resolution);
} /* muse_wave_calib_output_summary() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Find wavelength calibration solution on an arc frame.
  @param   aImage      holds the arc frame to be used for calibration
  @param   aTrace      table containing the tracing solution
  @param   aLinelist   table of expected arc lines
  @param   aParams     wavelength calibration parameters
  @return  The cpl_table * containing the wavelength calibration polynomials
           or NULL in case of error.

  Loop over all slices: first detect all arc emission lines in a column
  extracted from the center of the slice (on the filtered image), using
  muse_wave_lines_search(). Identify all detected arc lines using pattern
  matching against the input line list (with cpl_ppm_match_positions()).
  Then measure the position of each identified arc line outwards from the
  center to the edges of the slice (on the original input image), using
  muse_wave_line_fit_single(). Finally, derive a two-dimensional polynomial
  wavelength solution for each slice, using muse_wave_create_poly() and store
  it with muse_wave_poly_to_table().

  If aParams->rflag is true, the table aParams->residuals is created and filled
  using data on the fit iterations, by muse_wave_poly_fit().

  @note The FITS headers of the input image will be modified to contain
        ESO QC keywords of the wavelength calibration. Existing ESO QC
        keywords about slice properties will be removed.

  @qa Residuals of the polynomial fit will be a robust indicator for failures.
      To exclude that misidentifications of arc lines conspire to give good
      but wrong solutions, the fitting solutions of neighboring slices can be
      compared.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, input image is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         input image does not contain valid STAT extension}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL, trace table is NULL}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         trace table does not contain the right number of slices}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL, input arc line list is NULL}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         input parameters structure is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         vertical size of the data is below 4000 pixels}
  @error{set CPL_ERROR_BAD_FILE_FORMAT\, return NULL,
         loading reference wavelengths from table did not work}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_calib(muse_image *aImage, cpl_table *aTrace, cpl_table *aLinelist,
                muse_wave_params *aParams)
{
  if (!aImage) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "arc image missing!");
    return NULL;
  }
  unsigned char ifu = muse_utils_get_ifu(aImage->header);
  /* variance always has to be larger than zero */
  double minstat = cpl_image_get_min(aImage->stat);
  if (minstat <= 0.) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "arc image %d does"
                          " not have valid STAT extension in IFU %hhu (minimum "
                          "is %e)!", 0, ifu, minstat);
    return NULL;
  }

  if (!aTrace) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "trace table missing "
                          "for IFU %hhu, cannot create wavelength calibration!",
                          ifu);
    return NULL;
  } else if (cpl_table_get_nrow(aTrace) != kMuseSlicesPerCCD) {
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "trace table "
                          "not valid for this dataset of IFU %hhu!", ifu);
    return NULL;
  }

  if (!aLinelist) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "no arc line list "
                          "supplied for IFU %hhu!", ifu);
    return NULL;
  }
  if (!aParams) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "wavelength "
                          "calibration parameters missing for IFU %hhu!", ifu);
    return NULL;
  }
  if (aParams->fitweighting > MUSE_WAVE_WEIGHTING_CERRSCATTER) {
    cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE,
                          "unknown weighting scheme for IFU %hhu", ifu);
    return NULL;
  }

  /* do not support smaller mock datasets any more */
  int nx = cpl_image_get_size_x(aImage->data),
      ny = cpl_image_get_size_y(aImage->data);
  if (ny < 4000) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "this dataset of "
                          "IFU %hhu is too small (%dx%d pix) to be supported",
                          ifu, nx, ny);
    return NULL;
  }

  cpl_msg_info(__func__, "Using polynomial orders %hu (x) and %hu (y), %s "
               "weighting, assuming initial sampling of %.3f +/- %.3f Angstrom"
               "/pix, in IFU %hhu", aParams->xorder, aParams->yorder,
               muse_wave_weighting_string[aParams->fitweighting],
               kMuseSpectralSamplingA, aParams->ddisp, ifu);

  cpl_vector *vreflam = muse_wave_lines_get(aLinelist, 5, 0.);
  if (!vreflam) {
    cpl_error_set_message(__func__, CPL_ERROR_BAD_FILE_FORMAT, "could not "
                          "create list of FWHM reference arc wavelengths for "
                          "IFU %hhu", ifu);
    return NULL;
  }

  int debug = getenv("MUSE_DEBUG_WAVECAL")
            ? atoi(getenv("MUSE_DEBUG_WAVECAL")) : 0;
  char fn_debug[100];
  FILE *fp_debug = NULL;
  if (debug >= 3) {
    snprintf(fn_debug, 99, "MUSE_DEBUG_WAVE_LINES-%02hhu.ascii", ifu);
    cpl_msg_info(__func__, "Will write all single line fits to \"%s\"", fn_debug);
    fp_debug = fopen(fn_debug, "w");
    if (fp_debug) {
      fprintf(fp_debug,  "#slice x      y       lambda   lambdaerr\n");
    }
  }

  /* loop over all slices */
  cpl_propertylist_erase_regexp(aImage->header, "^"QC_WAVECAL_SLICE_PREFIX, 0);
  cpl_table *wavecaltable = NULL;
  unsigned short islice;
  for (islice = 0; islice < kMuseSlicesPerCCD; islice++) {
    if (debug > 0) {
      printf("\n\nSlice %d of IFU %hhu\n", (int)islice + 1, ifu);
      fflush(stdout);
    }
    /* get the tracing polynomials for this slice */
    cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
                                                                   islice + 1);
    if (!ptrace) {
      cpl_msg_warning(__func__, "slice %2d of IFU %hhu: tracing polynomials "
                      "missing!", (int)islice + 1, ifu);
      continue;
    }

    /* detect all lines in the center of the slice */
    int imid = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER],
                                             ny/2., NULL));
    if (imid < 1 || imid > nx) {
      cpl_msg_warning(__func__, "slice %2d of IFU %hhu: faulty trace polynomial"
                      " detected", (int)islice + 1, ifu);
      muse_trace_polys_delete(ptrace);
      continue; /* next slice */
    }
    /* create muse_image from the spectrum in which we want to search lines */
    const int kWidth = 3;
    int k, kcol1 = imid - kWidth/2, kcol2 = kcol1 + kWidth;
    muse_imagelist *collist = muse_imagelist_new();
    for (k = kcol1; k <= kcol2; k++) {
      muse_image *column = muse_image_new();
      column->data = cpl_image_extract(aImage->data, k, 1, k, ny);
      column->dq = cpl_image_extract(aImage->dq, k, 1, k, ny);
      column->stat = cpl_image_extract(aImage->stat, k, 1, k, ny);
      column->header = cpl_propertylist_new();
      muse_imagelist_set(collist, column, k - kcol1);
    }
    muse_image *column = muse_combine_median_create(collist);
    cpl_propertylist_append_string(column->header, "BUNIT",
                                   muse_pfits_get_bunit(aImage->header));
    muse_imagelist_delete(collist);

    /* now search, measure, and store the lines */
    cpl_table *detlines = muse_wave_lines_search(column, aParams->detsigma,
                                                 islice + 1, ifu);
    if (!detlines) {
      cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "problem when "
                            "searching for arc lines in slice %d of IFU %hhu, "
                            "columns %d..%d", (int)islice + 1, ifu, kcol1, kcol2);
      muse_image_delete(column);
      cpl_vector_delete(vreflam);
      muse_trace_polys_delete(ptrace);
      cpl_table_delete(wavecaltable);
      cpl_table_delete(aParams->residuals);
      aParams->residuals = NULL;
      return NULL;
    }
    /* clean up only here, to not disturb error message above */
    muse_image_delete(column);
    if (debug >= 2) {
      printf("Detected arc lines in slice %d of IFU %hhu:\n", (int)islice + 1, ifu);
      cpl_table_dump(detlines, 0, cpl_table_get_nrow(detlines), stdout);
      fflush(stdout);
    }

    /* identify the lines that we detected, and see if there are enough *
     * for the polynomial order in y-direction to fit a polynomial      */
    int nfound = cpl_table_get_nrow(detlines);
    /* get all arc lines, taking into account the detection limit */
    double minflux = cpl_table_get_column_min(detlines, "flux") * 1.15;
    cpl_vector *vlambda = muse_wave_lines_get(aLinelist, 1, minflux);
    muse_wave_lines_identify(detlines, vlambda, aParams);
    cpl_vector_delete(vlambda);
    int nlines = cpl_table_get_nrow(detlines);
    cpl_msg_debug(__func__, "Identified %d of %d arc lines in slice %d of IFU "
                  "%hhu (column %d, %d..%d)", nlines, nfound, (int)islice + 1, ifu,
                  imid, kcol1, kcol2);
    if (debug >= 2) {
      printf("Identified arc lines with wavelengths in slice %d of IFU %hhu:\n",
             (int)islice + 1, ifu);
      cpl_table_dump(detlines, 0, cpl_table_get_nrow(detlines), stdout);
      fflush(stdout);
    }

    /* the first QC parameter does even make sense without lines */
    char *keyword = cpl_sprintf(QC_WAVECAL_SLICEj_LINES_NDET,
                                (unsigned short)(islice + 1));
    cpl_propertylist_append_int(aImage->header, keyword, nfound);
    cpl_free(keyword);

    if (nlines < aParams->yorder + 1) {
      /* could apparently not identify enough lines, or an error occured */
      cpl_msg_error(__func__, "Could not identify enough arc lines in slice %d"
                    " of IFU %hhu (%d of %d, required %d)", (int)islice + 1, ifu,
                    nlines, nfound, (int)aParams->yorder + 1);
      muse_trace_polys_delete(ptrace);
      cpl_table_delete(detlines);
      continue; /* work on next slice immediately */
    }

    /* the next set of QC parameters */
    muse_wave_calib_qc_peaks(detlines, aImage->header, islice + 1);
    muse_wave_calib_qc_fwhm_old(detlines, aImage->header, vreflam, islice + 1);
    /* above we wrote the number of detected lines, now is the number of *
     * arc lines really used in the fit, i.e. the identified ones        */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_LINES_NID, (unsigned short)(islice + 1));
    cpl_propertylist_append_int(aImage->header, keyword, nlines);
    cpl_free(keyword);

    /* positions matrix and wavelengths vector to be used for the 2D polynomial  *
     * fit; just set and initial size of 1, it has to be resized with every line */
    cpl_matrix *xypos = cpl_matrix_new(2, 1);
    cpl_vector *lambdas = cpl_vector_new(1),
               *dlambdas = NULL;
    if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
      dlambdas = cpl_vector_new(1);
    }
    int ientry = 0; /* index for these two structures */

    /* for all identified arc lines, work on the original input data */
    int j;
    for (j = 0; j < nlines; j++) {
      /* convenient access to some properties of this identified line */
      double lambda = cpl_table_get(detlines, "lambda", j, NULL);
      double ypos = cpl_table_get(detlines, "center", j, NULL);
      /* better fix the Gaussian sigmas, otherwise *
       * the fit might run astray in low S/N cases */
      double sigma = cpl_table_get(detlines, "sigma", j, NULL);
      int n = 0,
          halfwidth = 2.*cpl_table_get(detlines, "fwhm", j, NULL); /* 2*FWHM */
      /* get both slice edges and the center */
      double dleft = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT],
                                            ypos, NULL),
             dright = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT],
                                             ypos, NULL),
             dmid = (dleft + dright) / 2.;
      int ileft = ceil(dleft),
          iright = floor(dright);
#if 0
      cpl_msg_debug(__func__, "limits at y=%f: %f < %f < %f", ypos, dleft, dmid,
                    dright);
#endif

      /* table to store line fits for this one arc line */
      cpl_table *fittable = muse_cpltable_new(muse_wavelines_def,
                                              (int)kMuseSliceHiLikelyWidth + 5);

      /* From the center of the slice move outwards and fit the line *
       * until we have arrived at the edge of the slice              */
      int i;
      for (i = dmid; i >= ileft; i--) {
        cpl_error_code rc = muse_wave_line_fit_single(aImage, i, ypos, halfwidth,
                                                      sigma, fittable, ++n);
        if (rc != CPL_ERROR_NONE) { /* do not count this line */
          --n;
        }
      }
#if 0
      printf("arc line j=%d, columns i=%d...", j + 1, i);
#endif
      for (i = dmid + 1; i <= iright; i++) {
        cpl_error_code rc = muse_wave_line_fit_single(aImage, i, ypos, halfwidth,
                                                      sigma, fittable, ++n);
        if (rc != CPL_ERROR_NONE) { /* do not count this line */
          --n;
        }
      }
      /* now remove rows with invalid entries, i.e. those that were not     *
       * filled with the properties of the fit -- cpl_table_erase_invalid() *
       * does not work, it deletes all columns                              */
      cpl_table_select_all(fittable);
      cpl_table_and_selected_invalid(fittable, "center");
      cpl_table_erase_selected(fittable);
#if 0
      printf("line %d, %d line fits\n", j + 1, i);
      cpl_table_dump(fittable, 0, n, stdout);
      fflush(stdout);
#endif
      cpl_errorstate state = cpl_errorstate_get();
      muse_wave_line_fit_iterate(fittable, -1, aParams);
      int npos = cpl_table_get_nrow(fittable);
      if (npos <= aParams->xorder) {
        cpl_msg_debug(__func__, "Polynomial fit failed in slice %d of IFU %hhu"
                      " for line at %.3fA (y-position near %.2f pix): %s",
                      (int)islice + 1, ifu, lambda, ypos, cpl_error_get_message());
        cpl_errorstate_set(state);
      }
#if 0
      else {
        printf("%s: line %2d, %d line fits, %d with low residuals:\n", __func__,
               j + 1, i, npos);
        cpl_table_dump(fittable, 0, npos, stdout);
        fflush(stdout);
      }
#endif

      /* resize matrix/vector to be able to fit all new entries */
      cpl_matrix_resize(xypos, 0, 0,
                        0, ientry + npos - cpl_matrix_get_ncol(xypos));
      cpl_vector_set_size(lambdas, ientry + npos);
      if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
        cpl_vector_set_size(dlambdas, ientry + npos);
      }

      /* now add the final fit positions */
      int ipos;
      for (ipos = 0; ipos < npos; ipos++) {
        /* set x-position (CCD column) in the first matrix row */
        cpl_matrix_set(xypos, 0, ientry, cpl_table_get(fittable, "x", ipos, NULL));
        /* y-position on CCD (center of Gauss fit) into second matrix row */
        cpl_matrix_set(xypos, 1, ientry, cpl_table_get(fittable, "center", ipos, NULL));
        /* the vector has to contain as many lambda entries */
        cpl_vector_set(lambdas, ientry, lambda);
        /* pretend that errors in Gaussian fit are errors in wavelength, *
         * scale according to the nominal Angstrom/pix sampling of MUSE, *
         * everything else would be too complicated for little gain      */
        if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
          cpl_vector_set(dlambdas, ientry, cpl_table_get(fittable, "cerr", ipos, NULL)
                                           * kMuseSpectralSamplingA);
        }

        ientry++; /* next position in this matrix/vector combo */
        if (fp_debug) {
          fprintf(fp_debug, "  %02d %04d %9.4f %10.4f %e\n", (int)islice + 1,
                 (int)cpl_table_get(fittable, "x", ipos, NULL),
                 cpl_table_get(fittable, "center", ipos, NULL), lambda,
                 cpl_table_get(fittable, "cerr", ipos, NULL) * kMuseSpectralSamplingA);
        }
      }
      cpl_table_delete(fittable);
    } /* for j (all identified arc lines) */
    cpl_table_delete(detlines);

    /* Compute two-dimensional wavelength solution for each slice. */
    cpl_polynomial *poly = NULL; /* polynomial for wavelength solution */
    double mse = 0; /* mean squared error */
    cpl_error_code rc = muse_wave_poly_fit(xypos, lambdas, dlambdas,
                                           &poly, &mse, aParams, islice + 1);
    cpl_matrix_delete(xypos);
    int nfinal = muse_cplvector_count_unique(lambdas);
    cpl_vector_delete(lambdas);
    cpl_vector_delete(dlambdas);
    /* above we wrote detected and identified lines, here *
     * save the ones that actually survived the fit       */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_FIT_NLINES, (unsigned short)(islice + 1));
    cpl_propertylist_append_int(aImage->header, keyword, nfinal);
    cpl_free(keyword);
    /* collect QC on wavelengths */
    muse_wave_calib_qc_lambda(aImage, islice + 1, ptrace, poly);
    /* trace polynomials are not needed further */
    muse_trace_polys_delete(ptrace);

    if (rc != CPL_ERROR_NONE) { /* failure */
      cpl_msg_warning(__func__, "Something went wrong while fitting in slice "
                      "%d of IFU %hhu: %s", (int)islice + 1, ifu,
                      cpl_error_get_message_default(rc));
      cpl_polynomial_delete(poly);
      continue; /* try next slice immediately */
    }

    if (!wavecaltable) {
      /* create output table with one row for each slice; no need to *
       * check return code, it can only fail for negative lengths    */
      wavecaltable = muse_wave_table_create(kMuseSlicesPerCCD,
                                            aParams->xorder, aParams->yorder);
    }
    rc = muse_wave_table_add_poly(wavecaltable, poly, mse,
                                  aParams->xorder, aParams->yorder, islice);

    /* use the number of wavelengths as indicator of the lines used for the *
     * fit and record it as QC parameter                                    */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_FIT_RMS, (unsigned short)(islice + 1));
    cpl_propertylist_append_float(aImage->header, keyword, sqrt(mse));
    cpl_free(keyword);

    if (rc != CPL_ERROR_NONE) {
      cpl_msg_warning(__func__, "Could not write polynomial to wavecal table "
                      "for slice %d of IFU %hhu: %s", (int)islice + 1, ifu,
                      cpl_error_get_message_default(rc));
    }
    /* clean up, ignore any failure with this */
    cpl_polynomial_delete(poly);
  } /* for islice (all kMuseSlicesPerCCD) */
  cpl_vector_delete(vreflam);
  if (fp_debug) {
    cpl_msg_info(__func__, "Done writing line fits to \"%s\"", fn_debug);
    fclose(fp_debug);
  }

  muse_wave_calib_output_summary(aImage->header, wavecaltable);
#if 0
  if (cpl_msg_get_level() == CPL_MSG_DEBUG) {
    cpl_table_dump(wavecaltable, 0, 3, stdout); fflush(stdout);
  }
#endif

  return wavecaltable;
} /* muse_wave_calib() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Find wavelength calibration solution using a list of arc images with
           different lamps.
  @param   aImages     the list of arc images to be used for calibration
  @param   aTrace      table containing the tracing solution
  @param   aLinelist   table of expected arc lines
  @param   aParams     wavelength calibration parameters
  @return  The cpl_table * containing the coefficients of the wavelength
           calibration polynomials or NULL in case of error.

  Check the input imagelist to make sure that the images all have the same
  size, and don't contain slice QC parameters.

  Loop over all slices: use tracing information to locate the vertical center
  of each slice. Loop over all input images (all lamps) and median-combine
  three CCD columns around the slice center to create a spectra which are then
  used separately to detect, using muse_wave_lines_search(), the arc emission
  lines.  Use the fluxes measured to select appropriate lines from the input
  linelist for identify and call muse_wave_lines_identify() to assign
  wavelengths. Create a combined, sorted list of such identified lines for all
  lamps, and use it to derive a first one-dimensional (vertical) wavelength
  solution.

  Loop through all lines suitable for measurement from the input linelist, use
  the 1D solution to determine each line position on the CCD. Fit Gaussians to
  each arc line in all CCD columns, using muse_wave_line_fit_single(), going
  outwards from the center of the slice to its edges. Use a horizontal
  one-dimensional polynomial fit, using muse_wave_line_fit_iterate(), to reject
  outliers in the measurements of each arc line.

  Finally, use all such measurements to derive the final two-dimensional
  wavelength solution for the slice, iteratively rejecting outliers, with
  muse_wave_poly_fit(). The polynomial is saved to the output table using
  muse_wave_table_add_poly().

  @note If aParams->rflag is true, the table aParams->residuals is created and
        filled using data on the fit iterations, by muse_wave_poly_fit().

  @note Output QC parameters are returned in the headers of the images in the
        input imagelist, previously present QC parameters about slices are
        deleted (using the QC_WAVECAL_SLICE_PREFIX macro).

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         input imagelist is NULL or empty}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL, trace table is NULL}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         trace table does not contain the right number of slices}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL, input arc line list is NULL}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         input parameters structure is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         vertical size of the data is below 4000 pixels}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         imagelist is not uniform}
  @error{set CPL_ERROR_BAD_FILE_FORMAT\, return NULL,
         loading reference wavelengths from table did not work}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, continue with next slice,
         searching for arc lines and line identification did not find enough lines}
  @error{propagate error code\, continue with next slice,
         deriving polynomial solution produced error}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_calib_lampwise(muse_imagelist *aImages, cpl_table *aTrace,
                         cpl_table *aLinelist, muse_wave_params *aParams)
{
  if (!aImages || muse_imagelist_get_size(aImages) < 1) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "arc imagelist is "
                          "missing or empty!");
    return NULL;
  }
  unsigned char ifu = muse_utils_get_ifu(muse_imagelist_get(aImages, 0)->header);
  if (!aTrace) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "trace table missing "
                          "for IFU %hhu, cannot create wavelength calibration!",
                          ifu);
    return NULL;
  } else if (cpl_table_get_nrow(aTrace) != kMuseSlicesPerCCD) {
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "trace table "
                          "not valid for this dataset of IFU %hhu!", ifu);
    return NULL;
  }
  if (!aLinelist) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "no arc line list "
                          "supplied for IFU %hhu!", ifu);
    return NULL;
  }
  if (!aParams) {
    cpl_error_set_message(__func__, CPL_ERROR_NULL_INPUT, "wavelength "
                          "calibration parameters missing for IFU %hhu!", ifu);
    return NULL;
  }
  if (aParams->fitweighting > MUSE_WAVE_WEIGHTING_CERRSCATTER) {
    cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE,
                          "unknown weighting scheme for IFU %hhu", ifu);
    return NULL;
  }

  muse_image *firstimage = muse_imagelist_get(aImages, 0);
  int nx = cpl_image_get_size_x(firstimage->data),
      ny = cpl_image_get_size_y(firstimage->data);
  /* do not support binned exposures */
  if (ny < 4000) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "this dataset of "
                          "IFU %hhu is too small (%dx%d pix) to be supported",
                          ifu, nx, ny);
    return NULL;
  }

  cpl_msg_info(__func__, "Using polynomial orders %hu (x) and %hu (y), %s "
               "weighting, assuming initial sampling of %.3f +/- %.3f Angstrom"
               "/pix, in IFU %hhu", aParams->xorder, aParams->yorder,
               muse_wave_weighting_string[aParams->fitweighting],
               kMuseSpectralSamplingA, aParams->ddisp, ifu);

  cpl_propertylist_erase_regexp(firstimage->header, "^"QC_WAVECAL_SLICE_PREFIX, 0);
  char *lamp = muse_utils_header_get_lamp_names(firstimage->header, ',');
  cpl_msg_debug(__func__, "Image 1 was taken with lamp %s", lamp);
  cpl_free(lamp);
  unsigned int k;
  for (k = 1; k < muse_imagelist_get_size(aImages); k++) {
    int nxk = cpl_image_get_size_x(muse_imagelist_get(aImages, k)->data),
        nyk = cpl_image_get_size_y(muse_imagelist_get(aImages, k)->data);
    if (nxk != nx || nyk != ny) {
      cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "arc "
                            "imagelist is not uniform (image %u is %dx%d, "
                            "first image is %dx%d)", k + 1, nxk, nyk, nx, ny);
      return NULL;
    }
    cpl_propertylist_erase_regexp(muse_imagelist_get(aImages, k)->header,
                                  "^"QC_WAVECAL_SLICE_PREFIX, 0);
    lamp = muse_utils_header_get_lamp_names(muse_imagelist_get(aImages, k)->header, ',');
    cpl_msg_debug(__func__, "Image %d was taken with lamp %s", k + 1, lamp);
    cpl_free(lamp);
  } /* for k (all images in list except first) */

  cpl_vector *vreflam = muse_wave_lines_get(aLinelist, 5, 0.);
  if (!vreflam) {
    cpl_error_set_message(__func__, CPL_ERROR_BAD_FILE_FORMAT, "could not "
                          "create list of FWHM reference arc wavelengths for "
                          "IFU %hhu", ifu);
    return NULL;
  }
  cpl_table *wavecal = NULL;
  int debug = getenv("MUSE_DEBUG_WAVECAL")
            ? atoi(getenv("MUSE_DEBUG_WAVECAL")) : 0;
  char fn_debug[100];
  FILE *fp_debug = NULL;
  if (debug >= 3) {
    snprintf(fn_debug, 99, "MUSE_DEBUG_WAVE_LINES-%02hhu.ascii", ifu);
    cpl_msg_info(__func__, "Will write all single line fits to \"%s\"", fn_debug);
    fp_debug = fopen(fn_debug, "w");
    if (fp_debug) {
      fprintf(fp_debug,  "#slice x      y       lambda   lambdaerr\n");
    }
  }

  /* loop over all slices */
  unsigned short islice;
  for (islice = 0; islice < kMuseSlicesPerCCD; islice++) {
    if (debug > 0) {
      printf("\n\nSlice %d of IFU %hhu\n", (int)islice + 1, ifu);
      fflush(stdout);
    }
    /* get the tracing polynomials for this slice */
    cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
                                                                   islice + 1);
    if (!ptrace) {
      cpl_msg_warning(__func__, "slice %2d of IFU %hhu: tracing polynomials "
                      "missing!", (int)islice + 1, ifu);
      continue;
    }
    /* detect all lines in the center of the slice */
    const int imid = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER],
                                                   ny/2., NULL)),
              iWidth = 3,
              icol1 = imid - iWidth/2,
              icol2 = icol1 + iWidth;
    if (imid < 1 || imid > nx) {
      cpl_msg_warning(__func__, "slice %2d of IFU %hhu: faulty trace polynomial"
                      "detected", (int)islice + 1, ifu);
      muse_trace_polys_delete(ptrace);
      continue; /* next slice */
    }
    cpl_table *detlines = NULL;
    int ndet = 0;
    for (k = 0; k < muse_imagelist_get_size(aImages); k++) {
      muse_image *arc = muse_imagelist_get(aImages, k);
      /* create muse_image from the spectrum in which we want to search lines */
      muse_imagelist *collist = muse_imagelist_new();
      int i;
      for (i = icol1; i <= icol2; i++) {
        muse_image *column = muse_image_new();
        column->data = cpl_image_extract(arc->data, i, 1, i, ny);
        column->dq = cpl_image_extract(arc->dq, i, 1, i, ny);
        column->stat = cpl_image_extract(arc->stat, i, 1, i, ny);
        muse_imagelist_set(collist, column, i - icol1);
      } /* for i (neighboring image columns) */
      muse_image *column = muse_combine_median_create(collist);
      cpl_propertylist_append_string(column->header, "BUNIT",
                                     muse_pfits_get_bunit(arc->header));
      muse_imagelist_delete(collist);

      /* now search, measure, and store the lines */
      cpl_table *detections = muse_wave_lines_search(column, aParams->detsigma,
                                                     islice + 1, ifu);
      muse_image_delete(column);
      int nlampdet = cpl_table_get_nrow(detections); /* det. with this lamp */
      ndet += nlampdet; /* detections of all lamps */
      char *lampname = muse_utils_header_get_lamp_names(arc->header, ',');
      cpl_table_fill_column_window_string(detections, "lampname", 0, nlampdet,
                                          lampname);
      cpl_array *lampnumbers = muse_utils_header_get_lamp_numbers(arc->header);
      cpl_table_fill_column_window_int(detections, "lampno", 0, nlampdet,
                                       cpl_array_get_int(lampnumbers, 0, NULL));
      cpl_array_delete(lampnumbers);
      /* get arc lines for this lamp from original list */
      double minflux = cpl_table_get_column_min(detections, "flux") * 1.15;
      cpl_vector *ionlambdas = muse_wave_lines_get_for_lamp(aLinelist, lampname,
                                                            1, minflux);
      cpl_free(lampname);
      muse_wave_lines_identify(detections, ionlambdas, aParams);
      cpl_vector_delete(ionlambdas);

      if (!detlines) {
        detlines = detections;
      } else {
        cpl_table_insert(detlines, detections, cpl_table_get_nrow(detlines));
        cpl_table_delete(detections);
      }
    } /* for k (all images in list) */
    cpl_propertylist *order = cpl_propertylist_new();
    cpl_propertylist_append_bool(order, "y", CPL_FALSE);
    cpl_table_sort(detlines, order);
    cpl_propertylist_delete(order);
    int nlines = cpl_table_get_nrow(detlines);
    if (debug >= 2) {
      printf("Detected and identified %d arc lines in slice %d of IFU %hhu:\n",
             nlines, (int)islice + 1, ifu);
      cpl_table_dump(detlines, 0, cpl_table_get_nrow(detlines), stdout);
      fflush(stdout);
    }
    /* the first QC parameter does even make sense without lines */
    char *keyword = cpl_sprintf(QC_WAVECAL_SLICEj_LINES_NDET, (unsigned short)(islice + 1));
    cpl_propertylist_append_int(firstimage->header, keyword, ndet);
    cpl_free(keyword);

    if (nlines < aParams->yorder + 1) {
      /* could apparently not identify enough lines, or an error occured */
      cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "could not "
                            "detect and/or identify enough arc lines in slice "
                            "%d of IFU %hhu (%d of %d, required %d)",
                            (int)islice + 1, ifu, nlines, ndet, (int)aParams->yorder + 1);
      muse_trace_polys_delete(ptrace);
      cpl_table_delete(detlines);
      continue; /* work on next slice immediately */
    }
    cpl_msg_info(__func__, "Identified %d of %d detected arc lines in slice %d"
                 " of IFU %hhu (column %d, %d..%d)", nlines, ndet, (int)islice + 1,
                 ifu, imid, icol1, icol2);

    /* the next set of QC parameters */
    muse_wave_calib_qc_peaks(detlines, firstimage->header, islice + 1);
    /* above we wrote the number of detected lines, now is the number of *
     * arc lines really used in the fit, i.e. the identified ones        */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_LINES_NID, (unsigned short)(islice + 1));
    cpl_propertylist_append_int(firstimage->header, keyword, nlines);
    cpl_free(keyword);

    /* convert y and lambda into matrix and vector for an initial, *
     * vertical, inverse 1D fit                                    */
    cpl_vector *cen = cpl_vector_new(nlines);
    cpl_matrix *lbda = cpl_matrix_new(1, nlines);
    int idx;
    for (idx = 0; idx < nlines; idx++) {
      cpl_vector_set(cen, idx, cpl_table_get(detlines, "center", idx, NULL));
      cpl_matrix_set(lbda, 0, idx, cpl_table_get(detlines, "lambda", idx, NULL));
    }
#if 0
    char *fn = cpl_sprintf("slice%02d_lbda1.dat", (int)islice + 1);
    FILE *file = fopen(fn, "w");
    cpl_matrix_dump(lbda, file);
    fclose(file);
    cpl_free(fn);
#endif
    double mse1d, chisq1d;
    cpl_polynomial *fit
      = muse_utils_iterate_fit_polynomial(lbda, cen, NULL, detlines,
                                          aParams->yorder, 3.,
                                          &mse1d, &chisq1d);
#if 0
    cpl_vector *res = cpl_vector_new(cpl_vector_get_size(cen));
    cpl_vector_fill_polynomial_fit_residual(res, cen, NULL, fit, lbda, NULL);
    cpl_bivector *biv = cpl_bivector_wrap_vectors(cen, res);
    cpl_plot_bivector(NULL, NULL, NULL, biv);
    cpl_bivector_unwrap_vectors(biv);
    cpl_vector_delete(res);
#endif
    if (debug >= 1) {
      printf("Initial (inverse) polynomial fit in slice %2d of IFU %hhu "
             "(RMS = %f, chisq = %e, %d of %d input points left)\n", (int)islice + 1,
             ifu, sqrt(mse1d), chisq1d, (int)cpl_vector_get_size(cen), nlines);
      cpl_polynomial_dump(fit, stdout);
      fflush(stdout);
    }
    nlines = cpl_vector_get_size(cen);
#if 0
    fn = cpl_sprintf("slice%02hu_lbda2.dat", islice + 1);
    file = fopen(fn, "w");
    cpl_matrix_dump(lbda, file);
    fclose(file);
    cpl_free(fn);
#endif
    cpl_vector_delete(cen);
    cpl_matrix_delete(lbda);

    /* positions matrix and wavelengths vector to be used for the 2D polynomial  *
     * fit; just set and initial size of 1, it has to be resized with every line */
    cpl_matrix *xypos = cpl_matrix_new(2, 1);
    cpl_vector *lambdas = cpl_vector_new(1),
               *dlambdas = NULL;
    if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
      dlambdas = cpl_vector_new(1);
    }
    int ientry = 0; /* index for these two structures */

    /* now measure all bright, single lines taken from the input linelist */
    cpl_table *fwhmtable = muse_cpltable_new(muse_wavelines_def, 0);
    cpl_table_set_column_unit(fwhmtable, "fwhm", "Angstrom");
    int j, narclines = cpl_table_get_nrow(aLinelist);
    for (j = 0; j < narclines; j++) {
      int quality = cpl_table_get_int(aLinelist, MUSE_LINE_CATALOG_QUALITY, j, NULL);
      if (quality <= 1) {
        continue; /* skip unwanted line */
      }

      /* use the lamp name to find the corresponding exposure for arc line */
      const char *lampname = muse_wave_lines_get_lampname(aLinelist, j);
      muse_image *arc = NULL;
      for (k = 0; k < muse_imagelist_get_size(aImages); k++) {
        arc = muse_imagelist_get(aImages, k);
        char *thislamp = muse_utils_header_get_lamp_names(arc->header, ',');
        cpl_boolean match = CPL_FALSE;
        if (lampname && thislamp) {
          match = strncmp(lampname, thislamp, strlen(lampname)) == 0;
        }
        cpl_free(thislamp);
        if (match) {
          break;
        }
      } /* for k (all images in list) */

      cpl_table *fittable = NULL;
      if (quality == 2) { /* multiplet */
        fittable = muse_wave_line_handle_multiplet(arc, aLinelist, j, fit, ptrace,
                                                   aParams, islice + 1, debug);
      } else {
        fittable = muse_wave_line_handle_singlet(arc, aLinelist, j, fit, ptrace,
                                                 aParams, islice + 1, debug);
      }
      if (!fittable) {
        continue;
      }
      int nnew = cpl_table_get_nrow(fittable);

      /* resize matrix/vector to be able to fit all new entries */
      cpl_matrix_resize(xypos, 0, 0,
                        0, ientry + nnew - cpl_matrix_get_ncol(xypos));
      cpl_vector_set_size(lambdas, ientry + nnew);
      if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
        cpl_vector_set_size(dlambdas, ientry + nnew);
      }

      /* now add the final fit positions */
      cpl_table_set_column_unit(fittable, "fwhm", ""); /* ongoing unit conversion */
      int ipos;
      for (ipos = 0; ipos < nnew; ipos++) {
        /* set x-position (CCD column) in the first matrix row */
        cpl_matrix_set(xypos, 0, ientry, cpl_table_get(fittable, "x", ipos, NULL));
        /* y-position on CCD (center of Gauss fit) into second matrix row */
        cpl_matrix_set(xypos, 1, ientry, cpl_table_get(fittable, "center", ipos, NULL));
        /* the vector has to contain as many lambda entries */
        double lambda = cpl_table_get(fittable, "lambda", ipos, NULL);
        cpl_vector_set(lambdas, ientry, lambda);
        /* Pretend that errors in Gaussian fit are errors in wavelength,    *
         * use the first-guess 1D solution to convert from pix to Angstrom. */
        double dlbda;
        cpl_polynomial_eval_1d(fit, lambda, &dlbda); /* not interested in result */
        double cerr = 0.;
        if (aParams->fitweighting != MUSE_WAVE_WEIGHTING_UNIFORM) {
          cerr = cpl_table_get(fittable, "cerr", ipos, NULL) / dlbda;
          cpl_vector_set(dlambdas, ientry, cerr);
        }
        /* now that we have the actual sampling, also convert FWHM to Angstrom */
        int err;
        double fwhm = cpl_table_get(fittable, "fwhm", ipos, &err) / dlbda;
        if (!err) {
          cpl_table_set(fittable, "fwhm", ipos, fwhm);
        }
        ientry++; /* next position in this matrix/vector combo */
        if (fp_debug) {
          fprintf(fp_debug, "  %02d %04d %9.4f %10.4f %e\n", (int)islice + 1,
                 (int)cpl_table_get(fittable, "x", ipos, NULL),
                 cpl_table_get(fittable, "center", ipos, NULL), lambda, cerr);
        }
      } /* for ipos (all fittable rows) */
      cpl_table_set_column_unit(fittable, "fwhm", "Angstrom");
      /* append relevant lines to extra table to compute FWHM and R later */
      cpl_table_select_all(fittable);
      cpl_table_and_selected_invalid(fittable, "fwhm");
      cpl_table_erase_selected(fittable);
      cpl_table_insert(fwhmtable, fittable, cpl_table_get_nrow(fwhmtable));
      cpl_table_delete(fittable);
    } /* for j (all arc lines) */
    /* reset original line quality info */
    cpl_table_abs_column(aLinelist, MUSE_LINE_CATALOG_QUALITY);
    cpl_table_delete(detlines);
    cpl_polynomial_delete(fit);
    muse_wave_calib_qc_fwhm(fwhmtable, firstimage->header, islice + 1);
    cpl_table_delete(fwhmtable);

    /* Compute two-dimensional wavelength solution for each slice. */
    cpl_polynomial *poly = NULL; /* polynomial for wavelength solution */
    double mse2d;
    cpl_error_code rc = muse_wave_poly_fit(xypos, lambdas, dlambdas,
                                           &poly, &mse2d, aParams, islice + 1);
    cpl_msg_info(__func__, "Polynomial fit of %"CPL_SIZE_FORMAT" of %d positions"
                 " in slice %d of IFU %hhu gave an RMS of %f Angstrom",
                 cpl_vector_get_size(lambdas), ientry, (int)islice + 1, ifu,
                 sqrt(mse2d));
    cpl_matrix_delete(xypos);
    int nfinal = muse_cplvector_count_unique(lambdas);
    cpl_vector_delete(lambdas);
    cpl_vector_delete(dlambdas);
    /* above we wrote detected and identified lines, here *
     * save the ones that actually survived the fit       */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_FIT_NLINES, (unsigned short)(islice + 1));
    cpl_propertylist_append_int(firstimage->header, keyword, nfinal);
    cpl_free(keyword);
    /* collect QC on wavelengths */
    muse_wave_calib_qc_lambda(firstimage, islice + 1, ptrace, poly);
    /* trace polynomials are not needed further */
    muse_trace_polys_delete(ptrace);

    if (rc != CPL_ERROR_NONE) { /* failure of the polynomial fit */
      cpl_error_set_message(__func__, rc, "a failure while computing the two-di"
                            "mensional polynomial fit in slice %d of IFU %hhu",
                            (int)islice + 1, ifu);
      cpl_polynomial_delete(poly);
      continue; /* try next slice immediately */
    }

    if (!wavecal) {
      /* create output table with one row for each slice; no need to *
       * check return code, it can only fail for negative lengths    */
      wavecal = muse_wave_table_create(kMuseSlicesPerCCD,
                                       aParams->xorder, aParams->yorder);
    }
    rc = muse_wave_table_add_poly(wavecal, poly, mse2d,
                                  aParams->xorder, aParams->yorder, islice);

    /* use the number of wavelengths as indicator of the lines used for the *
     * fit and record it as QC parameter                                    */
    keyword = cpl_sprintf(QC_WAVECAL_SLICEj_FIT_RMS, (unsigned short)(islice + 1));
    cpl_propertylist_append_float(firstimage->header, keyword, sqrt(mse2d));
    cpl_free(keyword);

    if (rc != CPL_ERROR_NONE) {
      cpl_msg_warning(__func__, "Could not write polynomial to wavecal table "
                      "for slice %d of IFU %hhu: %s", (int)islice + 1, ifu,
                      cpl_error_get_message_default(rc));
    }
    /* clean up, ignore any failure with this */
    cpl_polynomial_delete(poly);
  } /* for islice (all kMuseSlicesPerCCD) */
  cpl_vector_delete(vreflam);
  if (fp_debug) {
    cpl_msg_info(__func__, "Done writing line fits to \"%s\"", fn_debug);
    fclose(fp_debug);
  }

  muse_wave_calib_output_summary(firstimage->header, wavecal);
  /* copy the QC parameters to the other image headers */
  for (k = 1; k < muse_imagelist_get_size(aImages); k++) {
    cpl_propertylist *header = muse_imagelist_get(aImages, k)->header;
    cpl_propertylist_erase_regexp(header, "^"QC_WAVECAL_SLICE_PREFIX, 0);
    cpl_propertylist_copy_property_regexp(header, firstimage->header, "^ESO QC", 0);
  } /* for k (all images in list except first) */

  return wavecal;
} /* muse_wave_calib_lampwise() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Check that a LINE_CATALOG has the expected format.
  @param   aTable    the line list table in MUSE format (table + header)
  @return  CPL_TRUE when format matches expectations, CPL_FALSE otherwise.

  Expected format currently means that the MUSE_LINE_CATALOG_LAMBDA and
  MUSE_LINE_CATALOG_QUALITY table columns are present, and that the header has
  an entry "VERSION = 3".

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aTable is NULL or empty or aHeader is NULL}
  @error{output error message\, set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         input table does not contain the required columns}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         table does not contain a VERSION header}
 */
/*----------------------------------------------------------------------------*/
cpl_boolean
muse_wave_lines_check(muse_table *aTable)
{
  cpl_ensure(aTable && aTable->table && aTable->header, CPL_ERROR_NULL_INPUT,
             CPL_FALSE);
  /* check the table */
  int nrow = cpl_table_get_nrow(aTable->table);
  cpl_ensure(nrow > 0, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  cpl_ensure(muse_cpltable_check(aTable->table, muse_line_catalog_def)
             == CPL_ERROR_NONE, CPL_ERROR_DATA_NOT_FOUND, CPL_FALSE);

  /* check the version */
  if (!cpl_propertylist_has(aTable->header, MUSE_HDR_LINE_CATALOG_VERSION)) {
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "%s does not "
                          "contain a VERSION header entry!",
                          MUSE_TAG_LINE_CATALOG);
    return CPL_FALSE;
  }
  int version = cpl_propertylist_get_int(aTable->header,
                                         MUSE_HDR_LINE_CATALOG_VERSION);
#define LINE_CATALOG_EXPECTED_VERSION 3
  if (version != LINE_CATALOG_EXPECTED_VERSION) {
    cpl_error_set_message(__func__, CPL_ERROR_BAD_FILE_FORMAT, "VERSION = %d "
                          "is wrong, we need a %s with VERSION = %d", version,
                          MUSE_TAG_LINE_CATALOG, LINE_CATALOG_EXPECTED_VERSION);
    return CPL_FALSE;
  }
  return CPL_TRUE;
} /* muse_wave_lines_check() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Load usable wavelengths from a linelist table into a vector.
  @param   aTable           table containing the linelist
  @param   aGoodnessLimit   lower limit of the goodness of a line
  @param   aFluxLimit       lower limit of the flux of a line
  @return  the cpl_vector * containing the wavelengths of usable lines
           or NULL in case of error

  This uses the column MUSE_LINE_CATALOG_LAMBDA for the wavelengths and column
  MUSE_LINE_CATALOG_QUALITY for the quality setting. It expects that the input
  table is sorted by increasing wavelength. If lines occur that are closer
  together than 1.5 Angstrom, they are removed from the output.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, input table is NULL or empty}
  @error{output error message\, set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         input table does not contain the required columns}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         table is not sorted by increasing wavelength}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         no limes passing aGoodnessLimit were found}
 */
/*----------------------------------------------------------------------------*/
cpl_vector *
muse_wave_lines_get(cpl_table *aTable, int aGoodnessLimit, double aFluxLimit)
{
  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, NULL);
  int nrow = cpl_table_get_nrow(aTable);
  cpl_ensure(nrow > 0, CPL_ERROR_NULL_INPUT, NULL);

  cpl_ensure(cpl_table_has_column(aTable, MUSE_LINE_CATALOG_LAMBDA) == 1
             && cpl_table_has_column(aTable, MUSE_LINE_CATALOG_QUALITY) == 1,
             CPL_ERROR_DATA_NOT_FOUND, NULL);

  /* convert the usable wavelengths into a vector */
  cpl_vector *lambdas = cpl_vector_new(nrow);
  int i, pos = 0;
  for (i = 0; i < nrow; i++) {
    double lambda = cpl_table_get(aTable, MUSE_LINE_CATALOG_LAMBDA, i, NULL),
           flux = cpl_table_get(aTable, MUSE_LINE_CATALOG_FLUX, i, NULL);

    /* make sure that the arc line table is sorted, to simplify debugging */
    if (i > 0 && lambda < cpl_table_get(aTable, MUSE_LINE_CATALOG_LAMBDA, i-1,
                                        NULL)) {
      cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "%s is not "
                            "sorted by increasing lambda (at row %d)!",
                            MUSE_TAG_LINE_CATALOG, i+1);
      cpl_vector_delete(lambdas);
      return NULL;
    }

    if (cpl_table_get(aTable, MUSE_LINE_CATALOG_QUALITY, i, NULL)
        < aGoodnessLimit || flux < aFluxLimit) {
      /* this is apparently a line that should not be used */
      continue;
    }
    cpl_vector_set(lambdas, pos, lambda);
    pos++;
  } /* for i (all table rows) */
  if (!pos) { /* setting the size to zero does not work! */
    cpl_vector_delete(lambdas);
    cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "No lines with "
                          "%s >= %d found", MUSE_LINE_CATALOG_QUALITY,
                          aGoodnessLimit);
    return NULL;
  }
  /* cut vector size to contain the usable wavelengths */
  cpl_vector_set_size(lambdas, pos);

  /* remove lines that are too close together, i.e. within 1.5 Angstrom */
  for (i = 0; i < cpl_vector_get_size(lambdas) - 1; i++) {
    double l1 = cpl_vector_get(lambdas, i),
           l2 = cpl_vector_get(lambdas, i + 1);
    if ((l2 - l1) < 1.5) {
      cpl_msg_debug(__func__, "Excluding lines at %.3f and %.3f (d = %.3f) "
                    "Angstrom", l1, l2, l2 - l1);
      muse_cplvector_erase_element(lambdas, i + 1);
      muse_cplvector_erase_element(lambdas, i);
      i--; /* stay at this index to see what moved here */
    }
  } /* for i (all vector entries) */
#if 0
  printf("%d arc lines in input list are usable:\n", pos);
  cpl_vector_dump(lambdas, stdout);
#endif
  cpl_msg_debug(__func__, "Using a list of %d %s arc lines (from %6.1f to %6.1f "
                "Angstrom)", pos,
                aGoodnessLimit == 1 ? "good"
                                    : (aGoodnessLimit == 5 ? "FWHM reference"
                                                           : "all"),
                cpl_vector_get(lambdas, 0),
                cpl_vector_get(lambdas, cpl_vector_get_size(lambdas) - 1));

  return lambdas;
} /* muse_wave_lines_get() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Load wavelengths for a given lamp from a linelist table into a vector.
  @param   aTable           table containing the linelist
  @param   aLamp            the name of the lamp
  @param   aGoodnessLimit   lower limit of the goodness of a line
  @param   aFluxLimit       lower limit of the flux of a line
  @return  the cpl_vector * containing the wavelengths of usable lines
           or NULL in case of error

  This extracts those entries in the table corresponding to the given lamp,
  using @ref muse_wave_lines_get_lampname(), and then calls @ref
  muse_wave_lines_get() to create the vector for them.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aTable is NULL or empty or aLamp is NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_vector *
muse_wave_lines_get_for_lamp(cpl_table *aTable, const char *aLamp,
                             int aGoodnessLimit, double aFluxLimit)
{
  cpl_ensure(aTable && aLamp, CPL_ERROR_NULL_INPUT, NULL);
  int nrow = cpl_table_get_nrow(aTable);
  cpl_ensure(nrow > 0, CPL_ERROR_NULL_INPUT, NULL);

  cpl_table_unselect_all(aTable);
  int i;
  for (i = 0; i < nrow; i++) {
    const char *lampname = muse_wave_lines_get_lampname(aTable, i);
    if (!strcmp(lampname, aLamp)) {
      cpl_table_select_row(aTable, i);
    }
  } /* for i (all table rows) */
  cpl_table *lamplines = cpl_table_extract_selected(aTable);
#if 0
  printf("lamplines for %s\n", aLamp);
  cpl_table_dump(lamplines, 0, 10000, stdout);
  fflush(stdout);
#endif
  cpl_vector *lamplambdas = muse_wave_lines_get(lamplines, aGoodnessLimit,
                                                aFluxLimit);
  cpl_table_delete(lamplines);
  return lamplambdas;
} /* muse_wave_lines_get_for_lamp() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Associate the ion listed in a linelist table row to a lamp name.
  @param   aTable           table containing the linelist
  @param   aIdx             the row index in the table
  @return  one of "HgCd", "Ne", or "Xe" on success, "Unknown_Lamp" on error.

  This just uses string comparison of the first two characters in the ion
  column to set the corresponding lamp name.

  @error{set CPL_ERROR_NULL_INPUT\, return "Unknown_Lamp", aTable is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return "Unknown_Lamp",
         row aIdx does not contain a valid ion}
 */
/*----------------------------------------------------------------------------*/
const char *
muse_wave_lines_get_lampname(cpl_table *aTable, const int aIdx)
{
  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, "Unknown_Lamp");
  const char *ion = cpl_table_get_string(aTable, "ion", aIdx);
  cpl_ensure(ion, CPL_ERROR_ILLEGAL_INPUT, "Unknown_Lamp");

  /* for the HgCd lamp we have "HgI", "CdI", and "Hg_or_Cd" */
  if (!strncmp(ion, "Hg", 2) || !strncmp(ion, "Cd", 2)) {
    return "HgCd";
  }
  /* for the Ne lamp we have only "NeI" */
  if (!strncmp(ion, "Ne", 2)) {
    return "Ne";
  }
  /* for the Xe lamp we have "XeI" and "XeII" */
  if (!strncmp(ion, "Xe", 2)) {
    return "Xe";
  }
  return "Unknown_Lamp";
} /* muse_wave_lines_get_lampname() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Check, if a given wavelength is covered by a given instrument mode.
  @param   aLambda   the wavelength to check
  @param   aMode     the MUSE instrument mode
  @return  CPL_TRUE is the mode covers the wavelength, CPL_FALSE otherwise.
 */
/*----------------------------------------------------------------------------*/
cpl_boolean
muse_wave_lines_covered_by_data(double aLambda, muse_ins_mode aMode)
{
  if ((aLambda > kMuseUsefulLambdaMax) || (aLambda < kMuseUsefulELambdaMin)) {
    return CPL_FALSE;
  }
  /* first check the modes without a gap */
  if ((aMode <= MUSE_MODE_WFM_NONAO_N) && (aLambda >= kMuseUsefulNLambdaMin)) {
    return CPL_TRUE;
  }
  if ((aMode == MUSE_MODE_WFM_NONAO_E) && (aLambda >= kMuseUsefulELambdaMin)) {
    return CPL_TRUE;
  }
  /* NFM does not have a gap in the lamp calibrations, either */
  if ((aMode == MUSE_MODE_NFM_AO_N) && (aLambda >= kMuseUsefulNFMLambdaMin)) {
    return CPL_TRUE;
  }
  /* now the AO modes, with gap */
  if ((aMode == MUSE_MODE_WFM_AO_E) && (aLambda >= kMuseUsefulELambdaMin) &&
      ((aLambda <= kMuseNaLambdaMin) || (aLambda >= kMuseNaLambdaMax))) {
    return CPL_TRUE;
  }
  if ((aMode >= MUSE_MODE_WFM_AO_N) && (aLambda >= kMuseUsefulAOLambdaMin) &&
      ((aLambda <= kMuseNa2LambdaMin) || (aLambda >= kMuseNa2LambdaMax))) {
    return CPL_TRUE;
  }
  /* whatever is left now, has to be outside the common wavelength range */
  return CPL_FALSE;
} /* muse_wave_lines_covered_by_data() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Search and store emission lines in a column of an arc frame.
  @param   aColumnImage   muse_image that holds the column
  @param   aSigma         sigma (median deviation) as criterion for a line
  @param   aSlice         slice number (for debug output)
  @param   aIFU           the IFU number (for output)
  @return  The arc line fit properties table or NULL on error.
  @remark  See DRLDesign document (6.6) for layout of returned table.

  This function uses the detection threshold of aSigma times median deviation
  above the (median) background value of the S/N values of the input spectrum.
  Before attempting this S/N-based detetion, the background of the spectrum is
  removed by subtracting a wide median-filtered version of the data.
  (If the incoming arc exposure was flat-field corrected, using the S/N ensures
  that no spurious noise peaks in the blue part of the spectrum are mistaken as
  arc lines.) The detections are used to create a mask of the line locations.
  Detections of single pixels are rejected. The center of each of the remaining
  locations is then used as a starting value to fit a Gaussian function, to
  derive a good first estimate of the line position.  Finally, the results of
  the Gaussian fits are evaluated and lines with improbable fits (too
  narrow/broad, too faint, uncertain position) are removed from the output.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, the input column image is NULL}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL, variance extension invalid}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL, aSigma <= 0}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_lines_search(muse_image *aColumnImage, double aSigma,
                       const unsigned short aSlice, const unsigned char aIFU)
{
#if !SEARCH_DEBUG_FILES
  UNUSED_ARGUMENT(aSlice);
#endif
  cpl_ensure(aColumnImage, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(cpl_image_get_min(aColumnImage->stat) > 0.,
             CPL_ERROR_DATA_NOT_FOUND, NULL);
  cpl_ensure(aSigma > 0., CPL_ERROR_ILLEGAL_INPUT, NULL);

#define SEARCH_SUBTRACT_BG 1
#if SEARCH_SUBTRACT_BG
  /* Subtract any large scale background using a median-filtered spectrum. *
   * In the longer run, maybe a polynomial fit to the median-smoothed data *
   * should be used instead, because it does not create extra noise.       */
  cpl_image *bgmedian = cpl_image_duplicate(aColumnImage->data);
  cpl_image_fill_noise_uniform(bgmedian, -FLT_MIN, FLT_MIN);
#define BG_KERNEL_WIDTH 51
  cpl_mask *filter = cpl_mask_new(1, BG_KERNEL_WIDTH);
  cpl_mask_not(filter);
  cpl_image_filter_mask(bgmedian, aColumnImage->data, filter, CPL_FILTER_MEDIAN,
                        CPL_BORDER_FILTER);
  cpl_mask_delete(filter);
#if SEARCH_DEBUG_FILES
  char fn[FILENAME_MAX];
  sprintf(fn, "column_slice%02hu.fits", aSlice);
  muse_image_save(aColumnImage, fn);
  sprintf(fn, "column_slice%02hu_bgmedian.fits", aSlice);
  cpl_image_save(bgmedian, fn, CPL_TYPE_FLOAT, NULL, CPL_IO_CREATE);
#endif
  cpl_image *bgsub = cpl_image_subtract_create(aColumnImage->data, bgmedian);
  cpl_image_delete(bgmedian);
#if SEARCH_DEBUG_FILES
  sprintf(fn, "column_slice%02hu_bgsub.fits", aSlice);
  cpl_image_save(bgsub, fn, CPL_TYPE_FLOAT, NULL, CPL_IO_CREATE);
#endif
#endif

  /* create the S/N image */
  cpl_image *N = cpl_image_power_create(aColumnImage->stat, 0.5), /* noise */
#if SEARCH_SUBTRACT_BG
            *SN = cpl_image_divide_create(bgsub, N); /* S/N image */
  cpl_image_delete(bgsub);
#else
            *SN = cpl_image_divide_create(aColumnImage->data, N); /* S/N image */
#endif
  cpl_image_delete(N);
#if SEARCH_DEBUG_FILES
  sprintf(fn, "column_slice%02hu_SN.fits", aSlice);
  cpl_image_save(SN, fn, CPL_TYPE_FLOAT, NULL, CPL_IO_CREATE);
#endif

  /* use median statistics of the S/N image to determine the detection limit */
  double mdev, median = cpl_image_get_median_dev(SN, &mdev),
         limitSN = fmax(0.1, median + aSigma*mdev);
  cpl_mask *mask = cpl_mask_threshold_image_create(SN, limitSN, FLT_MAX);
  cpl_size nlines = 0; /* number of detected lines */
  cpl_image *label = cpl_image_labelise_mask_create(mask, &nlines);
  cpl_mask_delete(mask);
#if SEARCH_DEBUG_FILES
  sprintf(fn, "column_slice%02hu_label.fits", aSlice);
  cpl_image_save(label, fn, CPL_TYPE_USHORT, NULL, CPL_IO_CREATE);
#endif
#if SEARCH_DEBUG || SEARCH_DEBUG_FILES
  double mean =  cpl_image_get_mean(SN),
         stdev = cpl_image_get_stdev(SN),
         min = cpl_image_get_min(SN),
         max = cpl_image_get_max(SN);
  cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" lines found; parameters: sigma=%f, "
                "med=%f+/-%f " "mean=%f+/-%f min/max=%f/%f limit(S/N)=%f", nlines,
                aSigma, median, mdev, mean, stdev, min, max, limitSN);
#endif
  cpl_image_delete(SN);

  /* create table to store the fit results of all the lines */
  cpl_table *linesresult = muse_cpltable_new(muse_wavelines_def, nlines);

  /* now loop through all the detected lines and measure them */
  int i;
  for (i = 0; i < nlines; i++) {
    /* create mask for this label to isolate line (note that i starts at 0!) */
    cpl_mask *linemask = cpl_mask_threshold_image_create(label, i+0.5, i+1.5);
    int masksize = cpl_mask_get_size_y(linemask);

    /* determine the edges of the labeled region and the center of the line */
    int j = 1;
    while (j <= masksize && cpl_mask_get(linemask, 1, j) == CPL_BINARY_0) {
      j++;
    }
    int lopix = j;
    while (j <= masksize && cpl_mask_get(linemask, 1, j) == CPL_BINARY_1) {
      j++;
    }
    int hipix = j - 1;
    double cenpix = (hipix + lopix) / 2.;
#if SEARCH_DEBUG
    int err;
    cpl_msg_debug(__func__, "line %d lo/hi/cen=%d,%d,%f: values=%f,%f", i + 1,
                  lopix, hipix, cenpix,
                  cpl_image_get(aColumnImage->data, 1, lopix, &err),
                  cpl_image_get(aColumnImage->data, 1, hipix, &err));
#endif
    cpl_mask_delete(linemask);
    if (lopix == hipix) {
      /* one pixel is not enough for a solid detection:       *
       * set to some very low area / flux to be deleted below */
      cpl_table_set(linesresult, "flux", i, -1.);
      continue;
    }

    /* Enlarge region to be sure to measure the whole line (correct *
     * width!). As long as the pixel values are smaller than on the *
     * edges, and we don't go too far this should be OK.            */
#define MAXENLARGE 5
    /* reference pixel value (lower edge of mask) */
    int err0, err1 = 0, err2 = 0;
    double valref = cpl_image_get(aColumnImage->data, 1, lopix, &err0);
    cpl_errorstate prestate = cpl_errorstate_get();
    j = lopix;
    double value = -FLT_MAX;
    while (err1 == 0 && value < valref && (lopix - j) <= MAXENLARGE) {
      j--; /* enlarge downwards */
      value = cpl_image_get(aColumnImage->data, 1, j, &err1);
    }
    lopix = j + 1; /* set new lower edge */

    /* reference pixel value (lower edge of mask) */
    valref = cpl_image_get(aColumnImage->data, 1, hipix, &err2);
    j = hipix;
    value = -FLT_MAX;
    while (err2 == 0 && value < valref && (j - hipix) <= MAXENLARGE) {
      j++; /* enlarge upwards */
      value = cpl_image_get(aColumnImage->data, 1, j, &err2);
    }
    hipix = j - 1; /* set new upper edge */
    if (lopix > hipix) {
      /* guard against weird data, which would cause vector creation to fail */
      continue;
    }
#if SEARCH_DEBUG
    cpl_msg_debug(__func__, "region=%d...%d, size=%d", lopix, hipix,
                  hipix - lopix + 1);
#endif
    if (err1 < 0 || err2 < 0) {
      cpl_errorstate_set(prestate); /* clean possible "Access beyond boundaries" */
    }

    /* fill vectors */
    cpl_vector *positions = cpl_vector_new(hipix - lopix + 1), /* for positions */
               *values = cpl_vector_new(hipix - lopix + 1), /* for pixel values */
               *valuessigma = cpl_vector_new(hipix - lopix + 1); /* sigma values */
    int k;
    for (j = lopix, k = 0; j <= hipix; j++, k++) {
      cpl_vector_set(positions, k, j);
      cpl_vector_set(values, k, cpl_image_get(aColumnImage->data, 1, j, &err0));
      /* when setting pixel value from stat extension, use     *
       * square root, here we need the sigma not the variance: */
      cpl_vector_set(valuessigma, k, sqrt(cpl_image_get(aColumnImage->stat,
                                                        1, j, &err0)));
    }

#if SEARCH_DEBUG
    cpl_bivector *biv = cpl_bivector_wrap_vectors(positions, values);
    cpl_bivector_dump(biv, stdout);
    cpl_bivector_unwrap_vectors(biv);
#endif

    /* fit Gaussian */
    /* compute position, sigma, area, background offset, and mean squared error */
    double center, cerr, sigma, area, bglevel, mse;
    cpl_matrix *covariance;
    prestate = cpl_errorstate_get();
    cpl_error_code rc = cpl_vector_fit_gaussian(positions, NULL, values,
                                                valuessigma, CPL_FIT_ALL,
                                                &center, &sigma, &area, &bglevel,
                                                &mse, NULL, &covariance);

    /* try to treat some possible problems */
    if (rc == CPL_ERROR_CONTINUE) { /* fit didn't converge */
      /* estimate position error as sigma^2/area as CPL docs suggest *
       * no warning necessary in this case, print a debug message    */
      cerr = sqrt(sigma * sigma / area);
      cpl_msg_debug(__func__, "Gaussian fit in slice %hu of IFU %hhu around "
                    "position %6.1f: %s", aSlice, aIFU, cenpix,
                    cpl_error_get_message());
#if DEBUG_GAUSSFIT
      if (cpl_msg_get_log_level() == CPL_MSG_DEBUG) {
        /* CPL_ERROR_CONTINUE often occurs if there is another peak on the edge *
         * of the input positions/data array, so print it out in debug mode     */
        cpl_bivector *bv = cpl_bivector_wrap_vectors(positions, values);
        printf("Gaussian fit: %f+/-%f, %f, %f, %f %f\nand the input data:\n",
               center, cerr, bglevel, area, sigma, mse);
        cpl_bivector_dump(bv, stdout);
        cpl_bivector_unwrap_vectors(bv);
        fflush(stdout);
      }
#endif
    } else if (rc == CPL_ERROR_SINGULAR_MATRIX || !covariance) {
      cerr = sqrt(sigma * sigma / area);
      cpl_msg_debug(__func__, "Gaussian fit in slice %hu of IFU %hhu around "
                    "position %6.1f: %s", aSlice, aIFU, cenpix,
                    cpl_error_get_message());
    } else if (rc != CPL_ERROR_NONE) {
      /* although this is unlikely to occur, better print some warning *
       * and do something to the data to give this fit less weight in  *
       * the final computation of the solution                         */
      cpl_msg_debug(__func__, "Gaussian fit in slice %hu of IFU %hhu around "
                    "position %6.1f: %s", aSlice, aIFU, cenpix,
                    cpl_error_get_message());
      cerr = 100.; /* set a high centering error */
    } else {
      /* no error returned, everything should have worked nicely *
       * take the centering error from the covariance matrix     */
      cerr = sqrt(cpl_matrix_get(covariance, 0, 0));
#if SEARCH_DEBUG
      cpl_matrix_dump(covariance, stdout);
#endif
      cpl_matrix_delete(covariance);
    }
    cpl_errorstate_set(prestate); /* now we can reset the status */
    if (fabs(center - cenpix) > MUSE_WAVE_LINES_SEARCH_SHIFT_WARN) {
      cpl_msg_debug(__func__, "Large shift in Gaussian centering in slice %hu "
                    "of IFU %hhu: initial %7.2f, fit %7.2f", aSlice, aIFU,
                    cenpix, center);
    }

    /* save all results of this fit into the output table   *
     * (no need to fill the x-position into the "x" column) */
    cpl_table_set(linesresult, "y", i, cenpix);
    /* set actual peak value in the table using cpl_vector_get_max() is safe,  *
     * because the vector is constructed to be small enough to not include     *
     * neighboring lines and so the likelihood of including cosmic rays is low */
    cpl_table_set(linesresult, "peak", i, cpl_vector_get_max(values));
    cpl_table_set(linesresult, "center", i, center);
    cpl_table_set(linesresult, "cerr", i, cerr);
    cpl_table_set(linesresult, "fwhm", i, CPL_MATH_FWHM_SIG * sigma);
    cpl_table_set(linesresult, "sigma", i, sigma);
    cpl_table_set(linesresult, "flux", i, area);
    cpl_table_set(linesresult, "bg", i, bglevel);
    cpl_table_set(linesresult, "mse", i, mse);

    cpl_vector_delete(positions);
    cpl_vector_delete(values);
    cpl_vector_delete(valuessigma);
#if SEARCH_DEBUG
    printf("%s: results of fit stored in table row %d:\n", __func__, i + 1);
    cpl_table_dump(linesresult, i, 1, stdout);
    fflush(stdout);
#endif
  } /* for i (all detected lines) */
  cpl_image_delete(label);

  /* again loop through all the table lines, and remove unlikely detections */
  cpl_table_unselect_all(linesresult);
  for (i = 0; i < cpl_table_get_nrow(linesresult); i++) {
    if (cpl_table_get(linesresult, "cerr", i, NULL) > kMuseArcMaxCenteringError ||
        cpl_table_get(linesresult, "fwhm", i, NULL) < kMuseArcMinFWHM ||
        cpl_table_get(linesresult, "fwhm", i, NULL) > kMuseArcMaxFWHM ||
        cpl_table_get(linesresult, "flux", i, NULL) < kMuseArcMinFlux) {
      cpl_table_select_row(linesresult, i);
    }
  } /* for i (all detected lines) */
  cpl_table_erase_selected(linesresult);

  return linesresult;
} /* muse_wave_lines_search() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Identify the wavelength of arc detected lines using pattern matching.
  @param   aLines     the arc line fit properties table
  @param   aLambdas   the reference arc line list
  @param   aParams    wavelength calibration parameters
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  The identified lines are stored in the input table aLines, non-identified
  lines are removed from the table, the wavelengths of the identified lines
  are appended in the "lambda" column.

  Of the parameter passed in aParams, ddisp, tolerance, and yorder are used here
  (read-only).

  @error{return CPL_ERROR_NULL_INPUT, aLines or aLambdas is NULL}
  @error{return CPL_ERROR_DATA_NOT_FOUND, no arc lines could be identified}
  @error{return CPL_ERROR_ILLEGAL_OUTPUT,
         could not identify enough arc lines to fit polynomials of order aParams->yorder}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_lines_identify(cpl_table *aLines, cpl_vector *aLambdas,
                         const muse_wave_params *aParams)
{
  cpl_ensure_code(aLines && aLambdas, CPL_ERROR_NULL_INPUT);

  /* convert detected lines from the table into a simple vector */
  int i, nlines = cpl_table_get_nrow(aLines);
  cpl_vector *vcenter = cpl_vector_new(nlines);
  for (i = 0; i < nlines; i++) {
    cpl_vector_set(vcenter, i, cpl_table_get(aLines, "center", i, NULL));
  } /* for i (all lines) */
#if 0
  cpl_vector_dump(vcenter, stdout);
#endif
  double dmin = kMuseSpectralSamplingA - kMuseSpectralSamplingA * aParams->ddisp,
         dmax = kMuseSpectralSamplingA + kMuseSpectralSamplingA * aParams->ddisp;
  cpl_bivector *id_lines = cpl_ppm_match_positions(vcenter, aLambdas, dmin, dmax,
                                                   aParams->tolerance, NULL, NULL);
#if 0
  cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" identified lines (of %"
                CPL_SIZE_FORMAT" detected lines and %"CPL_SIZE_FORMAT
                " from linelist)", cpl_bivector_get_size(id_lines),
                cpl_vector_get_size(vcenter), cpl_vector_get_size(aLambdas));
  cpl_bivector_dump(id_lines, stdout);
  fflush(stdout);
#endif
  cpl_vector_delete(vcenter);

#if 0
  printf("detected lines before cleanup:\n");
  cpl_table_dump(aLines, 0, nlines, stdout);
  fflush(stdout);
#endif
  /* now go through the detected lines and remove all unidentified */
  const double *id_center = cpl_bivector_get_x_data_const(id_lines),
               *id_lambda = cpl_bivector_get_y_data_const(id_lines);
  cpl_table_unselect_all(aLines);
  int j, nid = cpl_bivector_get_size(id_lines);
  for (i = 0, j = 0; i < cpl_table_get_nrow(aLines) && id_center && id_lambda; i++) {
#if 0
    cpl_msg_debug(__func__, "c=%f, l=%f, c_det=%f", id_center[j], id_lambda[j],
                  cpl_table_get(aLines, "center", i, NULL));
#endif
    if (j < nid && fabs(id_center[j] - cpl_table_get(aLines, "center", i, NULL))
                   < DBL_EPSILON) {
      /* found the same line, append the wavelength */
      cpl_table_set(aLines, "lambda", i, id_lambda[j]);
      j++;
      continue;
    }
    cpl_table_select_row(aLines, i); /* not matching, delete this line */
  } /* for i (detected lines) and j (identified lines) */
  cpl_table_erase_selected(aLines);
  cpl_bivector_delete(id_lines);
  int debug = getenv("MUSE_DEBUG_WAVECAL")
            ? atoi(getenv("MUSE_DEBUG_WAVECAL")) : 0;
  if (debug >= 2) {
    printf("identified %d lines, %"CPL_SIZE_FORMAT" after cleanup:\n", nid,
           cpl_table_get_nrow(aLines));
    cpl_table_dump(aLines, 0, nid, stdout);
    fflush(stdout);
  }
  nid = cpl_table_get_nrow(aLines);
  /* check that after identification enough lines are still there */
  if (nid <= 0) {
    return CPL_ERROR_DATA_NOT_FOUND;
  } else if (nid < aParams->yorder + 1) {
    return CPL_ERROR_ILLEGAL_OUTPUT;
  }
  return CPL_ERROR_NONE;
} /* muse_wave_lines_identify() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Handle fitting of all single lines across the columns a given slice.
  @param   aImage      image with the arc line to fit
  @param   aLinelist   list of arc lines
  @param   aIdx        index of the current line in aLineList
  @param   aPoly       first-guess polynomial wavelength solution in the middle
                       of the slice
  @param   aTrace      array of the three tracing polynomials
  @param   aParams     wavelength calibration parameters
  @param   aSlice      slice number (for debug output)
  @param   aDebug      debug level
  @return  A cpl_table containing all fit results on success or NULL on error.

  The first-guess y-position for the arc line at index aIdx in aLinelist is
  computed using aPoly. The slice edges are computed for this position using
  aTrace. Then muse_wave_line_fit_single() is called for each CCD column,
  moving from the center of the slice to both edges, using first-guess position
  and a default value (taken to be the nominal instrumental width) for the
  Gaussian sigma. If the arc lines at aIdx is not a FWHM reference line, the
  sigma passed is negative to signify a fixed sigma to the fitting procedure.
  The result from the previous (neighboring) CCD column is taken as first-guess
  position of the next column, if it did not significantly differ from the
  previous result. Fits that went wrong are ignored. Possibly deviant fits are
  rejected using a 1D polynomial fit of aParams->xorder, using
  muse_wave_line_fit_iterate().

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImage\, aLinelist\, aPoly\, or aTrace are NULL}
  @error{return NULL,
         first-guess y-position of the arc line is near the edge or even outside the image}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_line_handle_singlet(muse_image *aImage, cpl_table *aLinelist,
                              unsigned int aIdx, cpl_polynomial *aPoly,
                              cpl_polynomial **aTrace,
                              const muse_wave_params *aParams,
                              const unsigned short aSlice, int aDebug)
{
  cpl_ensure(aImage && aLinelist && aPoly && aTrace, CPL_ERROR_NULL_INPUT, NULL);

  /* Fix the Gaussian sigmas for lines that are not FWHM reference lines,  *
   * otherwise the fit might run astray in low S/N cases. A negative sigma *
   * signifies constant value to fitting routine.                          */
  double sigma = kMuseSliceSlitWidthA / kMuseSpectralSamplingA / CPL_MATH_FWHM_SIG;
  /* if not reference line, set it negative, so that it is fixed in the fit */
  if (cpl_table_get(aLinelist, MUSE_LINE_CATALOG_QUALITY, aIdx, NULL) != 5) {
    sigma *= -1;
  }
  int halfwidth = 3.*kMuseSliceSlitWidthA / kMuseSpectralSamplingA; /* 3*FWHM */

  /* convenient access to some properties of the current line */
  double lambda = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_LAMBDA, aIdx, NULL),
         ypos = cpl_polynomial_eval_1d(aPoly, lambda, NULL);
  if ((ypos - halfwidth) < 1 ||
      (ypos + halfwidth) > cpl_image_get_size_y(aImage->data)) {
    if (aDebug >= 2) {
      cpl_msg_debug(__func__, "%f is supposed to lie near %.3f in slice %2hu of"
                    " IFU %hhu, i.e. outside!", lambda, ypos, aSlice,
                    muse_utils_get_ifu(aImage->header));
    }
    return NULL;
  }
  if (aDebug >= 2) {
    cpl_msg_debug(__func__, "%f is supposed to lie near %.3f in slice %2hu of "
                  "IFU %hhu", lambda, ypos, aSlice,
                  muse_utils_get_ifu(aImage->header));
  }
  /* get both slice edges and the center */
  double dleft = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_LEFT],
                                        ypos, NULL),
         dright = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_RIGHT],
                                         ypos, NULL),
         dmid = (dleft + dright) / 2.;
  int ileft = ceil(dleft),
      iright = floor(dright);
#if 0
  cpl_msg_debug(__func__, "limits at y=%f: %f < %f < %f", ypos, dleft, dmid,
                dright);
#endif

  /* table to store line fits for this one arc line */
  cpl_table *fittable = muse_cpltable_new(muse_wavelines_def,
                                          (int)kMuseSliceHiLikelyWidth + 5);

  /* From the center of the slice move outwards and fit the line *
   * until we have arrived at the edge of the slice              */
  double y = ypos;
  int i, n = 0;
  for (i = dmid; i >= ileft; i--) {
    cpl_error_code rc = muse_wave_line_fit_single(aImage, i, y, halfwidth,
                                                  sigma, fittable, ++n);
    if (rc != CPL_ERROR_NONE) { /* do not count this line */
      --n;
    } else {
      double ynew = cpl_table_get(fittable, "center", n - 1, NULL);
      if (fabs(y - ynew) < MUSE_WAVE_LINE_HANDLE_SHIFT_LIMIT) {
        y = ynew;
      }
    }
  } /* for i (columns to left of slice middle) */
#if 0
  printf("arc line aIdx=%u, columns i=%d...", aIdx + 1, i);
#endif
  y = ypos; /* start at middle y position again */
  for (i = dmid + 1; i <= iright; i++) {
    cpl_error_code rc = muse_wave_line_fit_single(aImage, i, ypos, halfwidth,
                                                  sigma, fittable, ++n);
    if (rc != CPL_ERROR_NONE) { /* do not count this line */
      --n;
    } else {
      double ynew = cpl_table_get(fittable, "center", n - 1, NULL);
      if (fabs(y - ynew) < MUSE_WAVE_LINE_HANDLE_SHIFT_LIMIT) {
        y = ynew;
      }
    }
  } /* for i (columns to right of slice middle) */
  /* now remove rows with invalid entries, i.e. those that were not     *
   * filled with the properties of the fit -- cpl_table_erase_invalid() *
   * does not work, it deletes all columns                              */
  cpl_table_select_all(fittable);
  cpl_table_and_selected_invalid(fittable, "center");
  cpl_table_erase_selected(fittable);
  cpl_table_fill_column_window(fittable, "lambda", 0,
                               cpl_table_get_nrow(fittable), lambda);
#if 0
  printf("line %u, %d line fits\n", aIdx + 1, n);
  cpl_table_dump(fittable, 0, n, stdout);
  fflush(stdout);
#endif
  /* and reject deviant fits */
  muse_wave_line_fit_iterate(fittable, lambda, aParams);
  int npos = cpl_table_get_nrow(fittable);
  if (npos <= 0) {
    cpl_msg_warning(__func__, "Polynomial fit failed in slice %hu of IFU %hhu "
                    "for line %u (y-position near %.2f pix): %s", aSlice,
                    muse_utils_get_ifu(aImage->header), aIdx + 1, ypos,
                    cpl_error_get_message());
  }
#if 0
  else {
    printf("%s: line %2u, %d line fits, %d with low residuals:\n", __func__,
           aIdx + 1, n, npos);
    cpl_table_dump(fittable, 0, npos, stdout);
    fflush(stdout);
  }
#endif
  return fittable;
} /* muse_wave_line_handle_singlet() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Handle fitting of all multiplets across the columns a given slice.
  @param   aImage      image with the arc line to fit
  @param   aLinelist   list of arc lines
  @param   aIdx        index of the first line of the multiplet in aLineList
  @param   aPoly       first-guess polynomial wavelength solution in the middle
                       of the slice
  @param   aTrace      array of the three tracing polynomials
  @param   aParams     wavelength calibration parameters
  @param   aSlice      slice number (for debug output)
  @param   aDebug      debug level
  @return  A cpl_table containing all fit results on success or NULL on error.

  The remaining line(s) of the multiplet are searched in aLinelist, and their
  first-guess y-positions are computed using aPoly. The slice edges are
  computed for the average y-position using aTrace. Then
  muse_wave_line_fit_multiple() is called for each CCD column, moving from the
  center of the slice to both edges, using first-guess position and a default
  value (taken to be the nominal instrumental width) for the Gaussian sigma.
  The result from the previous (neighboring) CCD column is taken as first-guess
  position of the next column, if it did not significantly differ from the
  previous result. Fits that went wrong are ignored. Possibly deviant fits are
  rejected using a 1D polynomial fit of aParams->xorder, calling
  muse_wave_line_fit_iterate() separately for each of the arc lines of the
  multiplet.

  @note This function changes aLinelist to invert the sign on the
        MUSE_LINE_CATALOG_QUALITY column for all multiplet components already
        used.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImage\, aLinelist\, aPoly\, or aTrace are NULL}
  @error{return NULL,
         first-guess y-position of the arc lines is near the edge or even outside the image}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_line_handle_multiplet(muse_image *aImage, cpl_table *aLinelist,
                                unsigned int aIdx, cpl_polynomial *aPoly,
                                cpl_polynomial **aTrace,
                                const muse_wave_params *aParams,
                                const unsigned short aSlice, int aDebug)
{
  cpl_ensure(aImage && aLinelist && aPoly && aTrace, CPL_ERROR_NULL_INPUT, NULL);

  /* search for the other line(s) of the multiplet */
#define MULTIPLET_SEARCH_RANGE 40. /* 40 Angstrom for NeI 7024...7059 */
  unsigned int nlines = 1; /* for the moment we know of one line of the multiplet */
  double lbda1 = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_LAMBDA, aIdx, NULL);
  const char *lamp1 = muse_wave_lines_get_lampname(aLinelist, aIdx);
  cpl_vector *lambdas = cpl_vector_new(nlines),
             *fluxes = cpl_vector_new(nlines);
  cpl_vector_set(lambdas, 0, lbda1);
  cpl_vector_set(fluxes, 0, cpl_table_get(aLinelist, MUSE_LINE_CATALOG_FLUX,
                                          aIdx, NULL));
  double lambda;
  unsigned int j = aIdx;
  for (lambda = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_LAMBDA, ++j, NULL);
       fabs(lambda - lbda1) < MULTIPLET_SEARCH_RANGE;
       lambda = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_LAMBDA, ++j, NULL)) {
    int quality = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_QUALITY, j, NULL);
    const char *lamp = muse_wave_lines_get_lampname(aLinelist, j);
    if (quality == 2 && !strcmp(lamp1, lamp)) { /* duplet of the same lamp */
      nlines++;
      cpl_vector_set_size(lambdas, nlines);
      cpl_vector_set_size(fluxes, nlines);
      cpl_vector_set(lambdas, nlines - 1, lambda);
      double flux = cpl_table_get(aLinelist, MUSE_LINE_CATALOG_FLUX, j, NULL);
      cpl_vector_set(fluxes, nlines - 1, flux);
      /* invert the sign of the quality, so that this line is not found again */
      cpl_table_set(aLinelist, MUSE_LINE_CATALOG_QUALITY, j, -quality);
    }
  } /* for lambda */

  if (aDebug >= 2) {
    printf("found multiplet of lamp %s with %u lines:\n", lamp1, nlines);
    cpl_bivector *bv = cpl_bivector_wrap_vectors(lambdas, fluxes);
    cpl_bivector_dump(bv, stdout);
    cpl_bivector_unwrap_vectors(bv);
    fflush(stdout);
  }

  /* positive sigma: using negative value (= fixed parameter in fit) actually *
   * causes higher deviations, worse RMS in the global fit, so leave it free  */
  double sigma = kMuseSliceSlitWidthA / kMuseSpectralSamplingA / CPL_MATH_FWHM_SIG;
  int halfwidth = 3.*kMuseSliceSlitWidthA / kMuseSpectralSamplingA; /* 3*FWHM */

  cpl_vector *ypos = cpl_vector_new(nlines);
  for (j = 0; j < nlines; j++) {
    double yp = cpl_polynomial_eval_1d(aPoly, cpl_vector_get(lambdas, j), NULL);
    cpl_vector_set(ypos, j, yp);
  } /* for j */
  double ypos1 = cpl_vector_get(ypos, 0),
         ypos2 = cpl_vector_get(ypos, nlines - 1);
  cpl_bivector *peaks = cpl_bivector_wrap_vectors(ypos, fluxes);
  if ((ypos1 - halfwidth) < 1 ||
      (ypos2 + halfwidth) > cpl_image_get_size_y(aImage->data)) {
    if (aDebug >= 2) {
      cpl_msg_debug(__func__, "%f is supposed to lie at %.3f..%.3f in slice "
                    "%2hu of IFU %hhu, i.e. outside!", lambda, ypos1, ypos2,
                    aSlice, muse_utils_get_ifu(aImage->header));
    }
    cpl_bivector_delete(peaks);
    cpl_vector_delete(lambdas);
    return NULL;
  }
  if (aDebug >= 2) {
    cpl_msg_debug(__func__, "%f is supposed to lie at %.3f..%.3f in slice %2hu "
                  "of IFU %hhu", lambda, ypos1, ypos2, aSlice,
                  muse_utils_get_ifu(aImage->header));
  }
  /* get both slice edges and the center */
  double dleft = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_LEFT],
                                        (ypos1 + ypos2)/2., NULL),
         dright = cpl_polynomial_eval_1d(aTrace[MUSE_TRACE_RIGHT],
                                         (ypos1 + ypos2)/2., NULL),
         dmid = (dleft + dright) / 2.;
  int ileft = ceil(dleft),
      iright = floor(dright);

  /* table to store line fits for this one arc line */
  cpl_table *fittable = muse_cpltable_new(muse_wavelines_def,
                                          ((int)kMuseSliceHiLikelyWidth + 5) * nlines);

  /* From the center of the slice move outwards and fit the *
   * line until we have arrived at the edge of the slice    */
  cpl_bivector *bp = cpl_bivector_duplicate(peaks),
               *bpgood = cpl_bivector_duplicate(peaks);
  int i, n = 0;
  for (i = dmid; i >= ileft; i--) {
    n += nlines;
    cpl_error_code rc = muse_wave_line_fit_multiple(aImage, i, bp, lambdas,
                                                    halfwidth, sigma, fittable, n);
    if (rc != CPL_ERROR_NONE) { /* do not count this fit */
      cpl_bivector_delete(bp);
      bp = cpl_bivector_duplicate(bpgood);
      n -= nlines;
    } else {
      cpl_vector *shifts = cpl_vector_duplicate(cpl_bivector_get_x(bp));
      cpl_vector_subtract(shifts, cpl_bivector_get_x(bpgood));
      double shift = cpl_vector_get_median(shifts); /* may be non-const! */
      cpl_vector_delete(shifts);
      if (fabs(shift) < MUSE_WAVE_LINE_HANDLE_SHIFT_LIMIT) {
        cpl_bivector_delete(bpgood);
        bpgood = cpl_bivector_duplicate(bp);
      } else {
        cpl_bivector_delete(bp);
        bp = cpl_bivector_duplicate(bpgood);
      }
    } /* else (good fit) */
  } /* for i (columns to left of slice middle) */
  cpl_bivector_delete(bp);
  cpl_bivector_delete(bpgood);
  bp = cpl_bivector_duplicate(peaks);
  bpgood = cpl_bivector_duplicate(peaks);
  for (i = dmid + 1; i <= iright; i++) {
    n += nlines;
    cpl_error_code rc = muse_wave_line_fit_multiple(aImage, i, bp, lambdas,
                                                    halfwidth, sigma, fittable, n);
    if (rc != CPL_ERROR_NONE) { /* do not count this fit */
      cpl_bivector_delete(bp);
      bp = cpl_bivector_duplicate(bpgood);
      n -= nlines;
    } else {
      cpl_vector *shifts = cpl_vector_duplicate(cpl_bivector_get_x(bp));
      cpl_vector_subtract(shifts, cpl_bivector_get_x(bpgood));
      double shift = cpl_vector_get_median(shifts); /* may be non-const! */
      cpl_vector_delete(shifts);
      if (fabs(shift) < MUSE_WAVE_LINE_HANDLE_SHIFT_LIMIT) {
        cpl_bivector_delete(bpgood);
        bpgood = cpl_bivector_duplicate(bp);
      } else {
        cpl_bivector_delete(bp);
        bp = cpl_bivector_duplicate(bpgood);
      }
    } /* else (good fit) */
  } /* for i (columns to right of slice middle) */
  cpl_bivector_delete(bp);
  cpl_bivector_delete(bpgood);
  /* now remove rows with invalid entries, i.e. those that *
   * were not filled with the properties of the fit        */
  cpl_table_select_all(fittable);
  cpl_table_and_selected_invalid(fittable, "center");
  cpl_table_erase_selected(fittable);
  cpl_bivector_delete(peaks);
  /* and reject deviant fits */
  for (j = 0; j < nlines; j++) {
    muse_wave_line_fit_iterate(fittable, cpl_vector_get(lambdas, j), aParams);
  } /* for i */
  cpl_vector_delete(lambdas);

  return fittable;
} /* muse_wave_line_handle_multiplet() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Fit a Gaussian to a single emission line in an arc frame and do
           simple error handling.
  @param   aImage        muse_image that holds the arc frame
  @param   aX            the column in the arc frame
  @param   aY            the most likely position in the y-direction
  @param   aHalfWidth    half-width of the region to extract
  @param   aSigma        sigma of the Gaussian function to fit the arc line,
                         can be negative to signify handling as fixed parameter
  @param   aFitTable     table to which the result is written
  @param   aRowsNeeded   number of rows that the table needs to write this line
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.
  @remark  The return value is just a diagnostic; the actual result is
           returned in the aFitTable parameter (see DRLDesign document (6.6) for
           table layout).

  This function wraps cpl_vector_fit_gaussian() to fit a single arc emission
  line with a Gaussian function. The results of this fit are written to the
  input table (aFitTable), which is enlarged if necessary.

  @error{return CPL_ERROR_NULL_INPUT,
         aImage\, its data or stat components\, or aFitTable are NULL}
  @error{propagate return code of cpl_vector_fit_gaussian() or CPL_ERROR_ILLEGAL_OUTPUT,
         Gaussian fit failed (no covariance matrix)}
  @error{propagate return code of cpl_vector_fit_gaussian(),
         Gaussian fit failed (other cases)}
  @error{return CPL_ERROR_ACCESS_OUT_OF_RANGE,
         Gaussian fit gave unexpectedly large offset}
  @error{enlarge table, aFitTable has less rows than needed}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_line_fit_single(muse_image *aImage, int aX, double aY, int aHalfWidth,
                          double aSigma, cpl_table *aFitTable, int aRowsNeeded)
{
  cpl_ensure_code(aImage && aImage->data && aImage->stat && aFitTable,
                  CPL_ERROR_NULL_INPUT);
#if 0
  cpl_msg_debug(__func__, "%d %d %d -> %d, %d", aX, (int)aY, aHalfWidth,
                2*aHalfWidth+1, aRowsNeeded);
#endif
  /* determine area on the input image, copy area into the vectors */
  cpl_vector *positions = cpl_vector_new(2*aHalfWidth+1),
             *values = cpl_vector_new(2*aHalfWidth+1),
             *valuessigma = cpl_vector_new(2*aHalfWidth+1);
  int i, j, ny = cpl_image_get_size_y(aImage->data);
  for (i = (int)aY - aHalfWidth, j = 0; i <= (int)aY + aHalfWidth && i <= ny;
       i++, j++) {
    int err;
    cpl_vector_set(positions, j, i);
    cpl_vector_set(values, j, cpl_image_get(aImage->data, aX, i, &err));
    cpl_vector_set(valuessigma, j,
                   sqrt(cpl_image_get(aImage->stat, aX, i, &err)));
  }
#if 0
  printf("input vectors: positions, values\n");
  cpl_bivector *biv = cpl_bivector_wrap_vectors(positions, values);
  cpl_bivector_dump(biv, stdout);
  fflush(stdout);
  cpl_bivector_unwrap_vectors(biv);
#if 0
  printf("input vectors: values, valuessigma\n");
  biv = cpl_bivector_wrap_vectors(values, valuessigma);
  cpl_bivector_dump(biv, stdout);
  fflush(stdout);
  cpl_bivector_unwrap_vectors(biv);
#endif
#endif

  /* do the Gaussfit */
  cpl_errorstate prestate = cpl_errorstate_get();
  double center, cerror, area, bglevel, mse;
  cpl_matrix *covariance = NULL;
  cpl_fit_mode mode = CPL_FIT_ALL;
  double sigma = aSigma;
  if (sigma < 0) {
    mode = CPL_FIT_CENTROID | CPL_FIT_AREA | CPL_FIT_OFFSET;
    sigma *= -1;
  }
  cpl_error_code rc
    = cpl_vector_fit_gaussian(positions, NULL, values, valuessigma, mode,
                              &center, &sigma, &area, &bglevel, &mse, NULL,
                              &covariance);
#if 0
  printf("--> rc=%d (%d) %s %#x %#x\n", rc, CPL_ERROR_NONE,
         cpl_error_get_message(), covariance, &covariance);
  fflush(stdout);
#endif
  cpl_vector_delete(positions);
  cpl_vector_delete(values);
  cpl_vector_delete(valuessigma);
  /* Determine goodness of fit and try to treat some possible problems. *
   * (This is partly copied from muse_wave_lines_search().)             */
  /* if no error happened, take the centering error from the covariance matrix */
  if (covariance) {
    cerror = sqrt(cpl_matrix_get(covariance, 0, 0));
    cpl_matrix_delete(covariance);
  } else { /* a missing covariance matrix is bad */
    cpl_msg_debug(__func__, "Gauss fit produced no covariance matrix (y=%.3f in "
                  "column=%d): %s", aY, aX, cpl_error_get_message());
    cpl_errorstate_set(prestate);
    return rc == CPL_ERROR_NONE ? CPL_ERROR_ILLEGAL_OUTPUT : rc;
  }
  if (rc == CPL_ERROR_CONTINUE) { /* fit didn't converge */
    /* estimate position error as sigma^2/area as CPL docs *
     * suggest and pretend that everything worked fine     */
    cerror = sqrt(sigma * sigma / area);
    cpl_errorstate_set(prestate);
    rc = CPL_ERROR_NONE;
  }
  if (rc != CPL_ERROR_NONE) {
    /* some other, probably more serious error, don't use this fit */
    cpl_msg_debug(__func__, "Gauss fit failed with some error (y=%.3f in "
                  "column=%d): %s", aY, aX, cpl_error_get_message());
    cpl_errorstate_set(prestate);
    return rc;
  }
  if (fabs(center - aY) > MUSE_WAVE_LINE_FIT_MAXSHIFT) {
    /* too big shift, don't use this fit */
    cpl_msg_debug(__func__, "Gauss fit gave unexpectedly large offset "
                  "(shifted %.3f pix from y=%.3f in column=%d)",
                  center - aY, aY, aX);
    return CPL_ERROR_ACCESS_OUT_OF_RANGE;
  }

  /* store the results in the table, after making sure it is large enough */
  if (cpl_table_get_nrow(aFitTable) < aRowsNeeded) {
    cpl_table_set_size(aFitTable, aRowsNeeded);
  }
  cpl_table_set(aFitTable, "center", aRowsNeeded - 1, center);
  cpl_table_set(aFitTable, "cerr", aRowsNeeded - 1, cerror);
  cpl_table_set(aFitTable, "sigma", aRowsNeeded - 1, sigma);
  if (mode == CPL_FIT_ALL) { /* is FWHM reference line, store the FWHM */
    cpl_table_set(aFitTable, "fwhm", aRowsNeeded - 1, CPL_MATH_FWHM_SIG * sigma);
  }
  cpl_table_set(aFitTable, "flux", aRowsNeeded - 1, area);
  cpl_table_set(aFitTable, "bg", aRowsNeeded - 1, bglevel);
  cpl_table_set(aFitTable, "mse", aRowsNeeded - 1, mse);
  cpl_table_set(aFitTable, "x", aRowsNeeded - 1, aX);
  cpl_table_set(aFitTable, "y", aRowsNeeded - 1, aY);

#if 0
  printf("--> Gaussian fit: %f +/- %f, %f\n", center, cerror, area);
  fflush(stdout);
#endif
  return rc;
} /* muse_wave_line_fit_single() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Fit a multi-Gaussian to a multiplet of arc emission lines and do
           simple error handling.
  @param   aImage        muse_image that holds the arc frame
  @param   aX            the column in the arc frame
  @param   aPeaks        first-guess values of the peaks to fit (the bivector
                         components are y-position and flux)
  @param   aLambdas      wavelengths corresponding to the peaks
  @param   aHalfWidth    half-width of the region to extract
  @param   aSigma        common sigma of the multi-Gaussian function,
                         can be negative to signify handling as fixed parameter
  @param   aFitTable     table to which the result is written
  @param   aRowsNeeded   number of rows that the table needs to write this line
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.
  @remark  The return value is just a diagnostic; the actual result is
           returned in the aFitTable parameter (see DRLDesign document (6.6) for
           table layout).

  This function wraps muse_utils_fit_multigauss_1d() to fit multiple arc
  emission lines with a multi-Gaussian function. The results of this fit are
  written to the input table (aFitTable), which is enlarged as necessary.

  aPeaks is filled with the results of the fit, so they can be used as start
  for another fit in a neighboring column.

  @error{return CPL_ERROR_NULL_INPUT,
         aImage\, its data or stat components\, or aFitTable are NULL}
  @error{propagate return code of muse_utils_fit_multigauss_1d() or CPL_ERROR_ILLEGAL_OUTPUT,
         multi-Gaussian fit failed (no covariance matrix)}
  @error{propagate return code of muse_utils_fit_multigauss_1d(),
         multi-Gaussian fit failed (other cases)}
  @error{return CPL_ERROR_ACCESS_OUT_OF_RANGE,
         multi-Gaussian fit gave unexpectedly large offset}
  @error{return CPL_ERROR_ILLEGAL_OUTPUT,
         multi-Gaussian fit resulted in line(s) with negative fluxes}
  @error{enlarge table, aFitTable has less rows than needed}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_line_fit_multiple(muse_image *aImage, int aX, cpl_bivector *aPeaks,
                            cpl_vector *aLambdas, int aHalfWidth, double aSigma,
                            cpl_table *aFitTable, int aRowsNeeded)
{
  cpl_ensure_code(aImage && aImage->data && aImage->stat && aFitTable,
                  CPL_ERROR_NULL_INPUT);
  cpl_vector *ypeaks = cpl_bivector_get_x(aPeaks),
             *fluxes = cpl_bivector_get_y(aPeaks);
  int np = cpl_vector_get_size(ypeaks);
  double yp1 = cpl_vector_get(ypeaks, 0),
         yp2 = cpl_vector_get(ypeaks, np - 1);
  int i1 = floor(yp1),
      i2 = ceil(yp2);
#if 0
  cpl_msg_debug(__func__, "%d %d..%d %d -> %d", aX, i1, i2, aHalfWidth,
                aRowsNeeded);
#endif
  /* determine area on the input image, copy area into the vectors */
  int npoints = (i2 + aHalfWidth) - (i1 - aHalfWidth) + 1;
  cpl_vector *positions = cpl_vector_new(npoints),
             *values = cpl_vector_new(npoints),
             *valuessigma = cpl_vector_new(npoints);
  double minval = DBL_MAX; /* 1st-guess fit value */
  int i, j, ny = cpl_image_get_size_y(aImage->data);
  for (i = i1 - aHalfWidth, j = 0; i <= i2 + aHalfWidth && i <= ny; i++, j++) {
    int err;
    cpl_vector_set(positions, j, i);
    double value = cpl_image_get(aImage->data, aX, i, &err);
    cpl_vector_set(values, j, value);
    if (value < minval) {
      minval = value;
    }
    cpl_vector_set(valuessigma, j,
                   sqrt(cpl_image_get(aImage->stat, aX, i, &err)));
  }
  minval = minval > 0 ? minval : 0.;
#if 0
  printf("input vectors: positions, values\n");
  cpl_bivector *biv = cpl_bivector_wrap_vectors(positions, values);
  cpl_bivector_dump(biv, stdout);
  fflush(stdout);
  cpl_bivector_unwrap_vectors(biv);
#endif
  cpl_bivector *bvalues = cpl_bivector_wrap_vectors(values, valuessigma);

  /* do the multi-Gauss fit */
  /* parameters: slope, background, sigma, np * center, np * flux */
  cpl_vector *poly = cpl_vector_new(2);
  cpl_vector_set(poly, 0, minval);
  cpl_vector_set(poly, 1, 0.);
  /* record the index of the expected strongest line, and its position */
  cpl_array *afluxes = cpl_array_wrap_double(cpl_vector_get_data(fluxes), np);
  cpl_size imaxflux;
  cpl_array_get_maxpos(afluxes, &imaxflux);
  double yposmax = cpl_vector_get(ypeaks, imaxflux);
  cpl_array_unwrap(afluxes);
  /* do the fit, store the errorstate before */
  cpl_errorstate prestate = cpl_errorstate_get();
  double sigma = aSigma,
         mse, chisq;
  cpl_matrix *covariance;
  cpl_error_code rc = muse_utils_fit_multigauss_1d(positions, bvalues, ypeaks,
                                                   &sigma, fluxes, poly,
                                                   &mse, &chisq, &covariance);
  cpl_vector_delete(positions);
  cpl_bivector_delete(bvalues);

  /* Determine goodness of fit and try to treat some possible problems. *
   * (This is partly copied from muse_wave_lines_search().)             */
  /* if no error happened, take the centering error from the covariance matrix */
  if (!covariance) { /* a missing covariance matrix is bad */
    cpl_msg_debug(__func__, "Multi-Gauss fit produced no covariance matrix (y=%."
                  "3f..%.3f in column=%d): %s", yp1, yp2, aX, cpl_error_get_message());
    cpl_errorstate_set(prestate);
    cpl_vector_delete(poly);
    return rc == CPL_ERROR_NONE ? CPL_ERROR_ILLEGAL_OUTPUT : rc;
  }
  if (rc != CPL_ERROR_NONE) {
    /* some other, probably more serious error, don't use this fit */
    cpl_msg_debug(__func__, "Multi-Gauss fit failed with some error (y=%.3f..%."
                  "3f in column=%d): %s", yp1, yp2, aX, cpl_error_get_message());
    cpl_errorstate_set(prestate);
    cpl_matrix_delete(covariance);
    cpl_vector_delete(poly);
    return rc;
  }
  /* did the center of the brightest line shift too much? */
  double yfitmax = cpl_vector_get(ypeaks, imaxflux);
  if (fabs(yposmax - yfitmax) > MUSE_WAVE_LINE_FIT_MAXSHIFT) {
    cpl_msg_debug(__func__, "Multi-Gauss fit gave unexpectedly large offset "
                  "(shifted %.3f pix from y=%.3f..%.3f in column=%d)",
                  yposmax - yfitmax, yp1, yp2, aX);
    cpl_matrix_delete(covariance);
    cpl_vector_delete(poly);
    return CPL_ERROR_ACCESS_OUT_OF_RANGE;
  }
  /* are any of the fluxes negative? */
  double yfitmin = cpl_vector_get_min(fluxes);
  if (yfitmin < 0) {
    cpl_msg_debug(__func__, "Multi-Gauss fit gave negative flux (%e in "
                  "multiplet from y=%.3f..%.3f in column=%d)", yfitmin,
                  yp1, yp2, aX);
    cpl_matrix_delete(covariance);
    cpl_vector_delete(poly);
    return CPL_ERROR_ILLEGAL_OUTPUT;
  }

  /* store the results in the table, after making sure it is large enough */
  if (cpl_table_get_nrow(aFitTable) < aRowsNeeded) {
    cpl_table_set_size(aFitTable, aRowsNeeded);
  }
  /* first record the common values */
  cpl_table_fill_column_window(aFitTable, "mse", aRowsNeeded - np, np, mse);
  cpl_table_fill_column_window(aFitTable, "x", aRowsNeeded - np, np, aX);
  cpl_table_fill_column_window(aFitTable, "sigma", aRowsNeeded - np, np, sigma);
  /* multiplets are never FWHM reference lines, do not store FWHM */
  for (i = 0, j = aRowsNeeded - np; i < np; i++, j++) {
    cpl_table_set(aFitTable, "lambda", j, cpl_vector_get(aLambdas, i));
    cpl_table_set(aFitTable, "y", j, cpl_vector_get(ypeaks, i));
    double center = cpl_vector_get(ypeaks, i);
    cpl_table_set(aFitTable, "center", j, center);
    /* centroid errors are starting with row/column index 3 in the matrix, *
     * the first ones are the two polynomial coeffs and the sigma          */
    double cerror = sqrt(cpl_matrix_get(covariance, 3 + i, 3 + i));
    cpl_table_set(aFitTable, "cerr", j, cerror);
    cpl_table_set(aFitTable, "flux", j, cpl_vector_get(fluxes, i));
    /* evaluate the linear background level at the fitted center */
    double bg = cpl_vector_get(poly, 0) + cpl_vector_get(poly, 1) * center;
    cpl_table_set(aFitTable, "bg", j, bg);
  } /* for i, j */
  cpl_vector_delete(poly);
  cpl_matrix_delete(covariance);
#if 0
  printf("stored %d fitted multiplet values:\n", np);
  cpl_table_dump(aFitTable, aRowsNeeded - np - 2 < 0 ? 0 : aRowsNeeded - np - 2, np + 5, stdout);
  fflush(stdout);
#endif

  return rc;
} /* muse_wave_line_fit_multiple() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Use a low-order polynomial to find and discard bad values for line
           centroid fits of single arc line across one slice.
  @param   aFitTable    the table holding the fit results at all positions
  @param   aLambda      wavelength of the line to fit
  @param   aParams     wavelength calibration parameters
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.
  @note aFitTable is changed to return the fit result!

  This function fits a polynomial to all detected arc line centers and uses this
  to remove deviant values (that could be due to low S/N in single spectra or
  blemishes (bad pixels/cosmic ray hits) on the CCD.

  aLambda is used to select the correct entries for a single line in aFitTable.
  If aLambda is negative, this function assumes that aFitTable only contains
  fits of a single arc line, and skips the selection by wavelength.

  Of the parameter passed in aParams, xorder, linesigma, and fitweighting are
  used here (read-only). The fit done here is not actually weighted, no matter
  what aParams->fitweighting is set, but that setting decides in which way the
  "cerr" column of aFitTable is handled: nothing is done for
  MUSE_WAVE_WEIGHTING_UNIFORM and MUSE_WAVE_WEIGHTING_CERR, but for
  MUSE_WAVE_WEIGHTING_SCATTER the RMS of the polynomial fit replaces the
  original error and for MUSE_WAVE_WEIGHTING_CERRSCATTER the RMS is added to
  the original "cerr" in quadrature.
  In case the fit fails, this most likely happens when not enough points are
  present before or after iterating the fit, all output errors will be set to
  10, a value higher than can normally be reached.

  (Alternatively, one could use the polynomial to replace detected positions
  with interpolated values from the polynomial fit. But in that case one loses
  the positional error estimates of the Gaussian fit to the arc line, that's
  why this is not currently done.)

  @error{return CPL_ERROR_NULL_INPUT, aFitTable is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aFitTable has no entries}
  @error{use 2.5, aParams->linesigma is negative}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_line_fit_iterate(cpl_table *aFitTable, double aLambda,
                           const muse_wave_params *aParams)
{
  cpl_ensure_code(aFitTable, CPL_ERROR_NULL_INPUT);
  int npos = cpl_table_get_nrow(aFitTable);
  cpl_ensure_code(npos > 0, CPL_ERROR_ILLEGAL_INPUT);
  double rsigma = aParams->linesigma < 0 ? 2.5 : aParams->linesigma;

  /* select all entries with the requested wavelength */
  cpl_table *fittable = NULL;
  if (aLambda > 0) {
    cpl_table_unselect_all(aFitTable);
    cpl_table_or_selected_double(aFitTable, "lambda", CPL_EQUAL_TO, aLambda);
    npos = cpl_table_count_selected(aFitTable);
    cpl_ensure_code(npos > 0, CPL_ERROR_ILLEGAL_INPUT);
    fittable = cpl_table_extract_selected(aFitTable);
    cpl_table_erase_selected(aFitTable);
  } else {
    fittable = aFitTable;
  }

  /* convert "x" and "center" columns into matrix and vector for the fit */
  cpl_matrix *pos = cpl_matrix_new(1, npos);
  cpl_vector *val = cpl_vector_new(npos);
  int i;
  for (i = 0; i < npos; i++) {
    cpl_matrix_set(pos, 0, i, cpl_table_get(fittable, "x", i, NULL));
    cpl_vector_set(val, i, cpl_table_get(fittable, "center", i, NULL));
  }

  /* use the iterative polynomial field to throw out bad entries in fittable */
  cpl_errorstate state = cpl_errorstate_get();
  double mse;
  cpl_polynomial *fit = muse_utils_iterate_fit_polynomial(pos, val, NULL, fittable,
                                                          aParams->xorder, rsigma,
                                                          &mse, NULL);
  cpl_matrix_delete(pos);
  cpl_vector_delete(val);
  cpl_polynomial_delete(fit);
  if (!cpl_errorstate_is_equal(state)) {
    /* this implies less than xorder entries left, and is really bad; set the *
     * error to something high enough to basically exclude these points       */
    cpl_table_fill_column_window(fittable, "cerr",
                                 0, cpl_table_get_nrow(fittable), 10.);
  } else if (aParams->fitweighting == MUSE_WAVE_WEIGHTING_SCATTER) {
    /* replace the original centroid error with the fit RMS */
    cpl_table_fill_column_window(fittable, "cerr",
                                 0, cpl_table_get_nrow(fittable), sqrt(mse));
  } else if (aParams->fitweighting == MUSE_WAVE_WEIGHTING_CERRSCATTER) {
    /* add RMS of fit to individual error in quadrature: *
     *    cerr = sqrt(cerr^2 + mse)                      */
    cpl_table_power_column(fittable, "cerr", 2.); /* cerr^2 */
    cpl_table_add_scalar(fittable, "cerr", mse); /* + mse */
    cpl_table_power_column(fittable, "cerr", 0.5); /* sqrt() */
  }
#if 0
  printf("line at %.3f Angstrom: rms = %f (mean error %f)\n", aLambda,
         sqrt(mse), cpl_table_get_column_mean(fittable, "cerr"));
  cpl_table_dump(fittable, 0, npos, stdout);
  fflush(stdout);
#endif
  if (aLambda > 0) { /* need to copy results back into input table */
    cpl_table_insert(aFitTable, fittable, cpl_table_get_nrow(aFitTable));
    cpl_table_delete(fittable);
  }
  return CPL_ERROR_NONE;
} /* muse_wave_line_fit_iterate() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Compute x to the power of y.
  @param  x   The base
  @param  y   The power
  @return x to the power of y

  This function was copied from CPL's internal function cpl_tools_ipow(),
  and adapted to our case, where we only need non-negative exponents.
  The few shortcuts also seem to help runtime.
 */
/*----------------------------------------------------------------------------*/
static inline double
muse_wave_ipow(double x, unsigned int y)
{
  if (!y) {
    return 1;
  }
  if (y == 1) {
    return x;
  }
  double x2 = x*x;
  if (y == 2) {
    return x2;
  }
  if (y == 3) {
    return x2*x;
  }
  if (y == 4) {
    return x2*x2;
  }
  double result;
  /* Handle least significant bit in y here in order to avoid an unnecessary *
   * multiplication of x - which could cause an over- or underflow           */
  result = y & 1 ? x : 1.; /* 0^0 is 1, while any other power of 0 is 0 */
  while (y >>= 1) {
    x *= x;
    if (y & 1) { /* Process least significant bit in y */
      result *= x;
    }
  }
  return result;
} /* muse_wave_ipow() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Evaluate a two-dimensional polynomial
           (with order N in x and M in y).
  @param   xy   The evaluation point (array of two values)
  @param   p    The parameters defining the polynomial
                (array of 2 + (N+1)*(M+1) values)
  @param   f    The function value
  @return  0 on success

  The first two parameters in p[] are assumed to be the polynomial orders in x
  and y, the remaining ones are taken to be the polynomial coefficients, with
  the coefficients in the y-direction cycling faster.
 */
/*----------------------------------------------------------------------------*/
static int
muse_wave_poly_2d(const double xy[], const double p[], double *f)
{
  unsigned short xorder = p[0],
                 yorder = p[1];
  /* add up the polynomial orders */
  *f = 0;
  double x = xy[0],
         y = xy[1];
  unsigned int i, idx = 2;
  for (i = 0; i <= xorder; i++) {
    double xi = muse_wave_ipow(x, i);
    unsigned int j; /* y order */
    for (j = 0; j <= yorder; j++) {
      *f += p[idx++] * xi * muse_wave_ipow(y, j);
    } /* for j */
  } /* for i */
  return 0;
} /* muse_wave_poly_2d() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Evaluate the derivatives of a two-dimensional polynomial
           (with order N in x and M in y).
  @param   xy   The evaluation point (array of two values)
  @param   p    The parameters defining the polynomial
                (array of 2 + (N+1)*(M+1) values)
  @param   f    The values of the derivative (array of 2 + (N+1)*(M+1) values)
  @return  0 on success

  See muse_wave_poly_2d() for the order of the parameters in the arrays.
 */
/*----------------------------------------------------------------------------*/
static int
muse_wave_dpoly_2d(const double xy[], const double p[], double f[])
{
  unsigned short xorder = p[0],
                 yorder = p[1];
  f[0] = f[1] = 0; /* derivatives regarding the polynomial orders are zero */
  /* now the real derivatives, they don't contain the coefficients any more */
  double x = xy[0],
         y = xy[1];
  unsigned int i, idx = 2;
  for (i = 0; i <= xorder; i++) {
    double xi = muse_wave_ipow(x, i);
    unsigned int j; /* y order */
    for (j = 0; j <= yorder; j++) {
      f[idx++] = xi * muse_wave_ipow(y, j);
    } /* for j */
  } /* for i */
  return 0;
} /* muse_wave_poly_x2y5_deriv() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the wavelength solution from the sample positions and the
           respective wavelengths.
  @param   aXYPos      the matrix with the x- and y-positions
  @param   aLambdas    the vector with the wavelengths at all positions
  @param   aDLambdas   the vector with the wavelength errors at all positions
                       (can be NULL)
  @param   aPoly       the wavelength solution polynomial that is computed here
  @param   aMSE        the mean squared error of the solution, computed here
  @param   aParams     wavelength calibration parameters
  @param   aSlice      slice number (for use with aParams->residuals)
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  The polynomial is allocated within this function, it has to be deallocated
  using cpl_polynomial_delete(). If this function returns with a failure, no
  deallocations are necessary.

  The input list of measurements is checked against the instrument mode set in
  aParams->mode, to see if a line is actually giving a good measurement in that
  mode. This is meant to exclude lines at the blue end of the wavelength range
  but especially inside the NaD notch filter region. (If a line is detected
  there it might still be shifted in wavelength due to the strongly varying
  filter function.)

  The mean-squared error aMSE computed here is weighted using the inverse of the
  wavelength errors, if aDLambdas is given. The rejection limit used within each
  iteration is |aParams->fitsigma * aMSE|. The iterations converge and the
  result is returned, if one of the following is true:
  - an iteration results in an RMS below aParams->targetrms
  - the RMS has improved by only 0.001 Angstrom compared to the previous
    iteration
  - no residuals larger than the rejection limit are found

  Of the parameter passed in aParams, xorder, yorder, fitsigma, and targetrms
  are used here (read-only); the residuals table is created and filled by this
  function, if rflag is true.

  @error{return CPL_ERROR_NULL_INPUT, aPoly\, aXYPos\, or aLambdas are NULL}
  @error{return CPL_ERROR_ILLEGAL_OUTPUT, the polynomial fit failed}
  @error{use 3.0, aParams->fitsigma is negative}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_poly_fit(cpl_matrix *aXYPos, cpl_vector *aLambdas, cpl_vector *aDLambdas,
                   cpl_polynomial **aPoly, double *aMSE, muse_wave_params *aParams,
                   const unsigned short aSlice)
{
  cpl_ensure_code(aPoly && aXYPos && aLambdas, CPL_ERROR_NULL_INPUT);

  /* filter the input measurements by wavelengths, and exclude those *
   * outside the valid range, depending on the instrument mode       */
  int ii, nin = cpl_vector_get_size(aLambdas),
      nout = 0;
#if 0
  cpl_msg_debug(__func__, "Testing coverage for mode %d.", aParams->mode);
#endif
  for (ii = 0; ii < cpl_vector_get_size(aLambdas); ii++) {
    double lambda = cpl_vector_get(aLambdas, ii);
    if (muse_wave_lines_covered_by_data(lambda, aParams->mode)) {
      nout++;
      continue;
    }
#if 0
    cpl_msg_debug(__func__, "line at %.3f Angstrom outside range", lambda);
#endif
    cpl_matrix_erase_columns(aXYPos, ii, 1);
    muse_cplvector_erase_element(aLambdas, ii);
    if (aDLambdas) {
      muse_cplvector_erase_element(aDLambdas, ii);
    }
    ii--; /* stay at this position */
  }
  cpl_msg_debug(__func__, "%d of %d measurements should have valid wavelengths.",
                nout, nin);

#if 0
  printf("aXYPos (%"CPL_SIZE_FORMAT"x%"CPL_SIZE_FORMAT"):\n",
         cpl_matrix_get_ncol(aXYPos), cpl_matrix_get_nrow(aXYPos));
  cpl_matrix_dump(aXYPos, stdout);
  printf("aLambdas (%"CPL_SIZE_FORMAT"):\n", cpl_vector_get_size(aLambdas));
  cpl_vector_dump(aLambdas, stdout);
  printf("aDLambdas (%"CPL_SIZE_FORMAT"):\n", cpl_vector_get_size(aDLambdas));
  cpl_vector_dump(aDLambdas, stdout);
  fflush(stdout);
#endif
  double rsigma = aParams->fitsigma < 0 ? 3.0 : aParams->fitsigma;
  int debug = getenv("MUSE_DEBUG_WAVECAL")
            ? atoi(getenv("MUSE_DEBUG_WAVECAL")) : 0;
  /* prepare the polynomial for two dimensions */
  *aPoly = cpl_polynomial_new(2);

  double rms = -1; /* start with negative (= invalid) RMS */
  int large_residuals = 1, /* init to force a first fit */
      niter = 1;
  while (large_residuals > 0) {
    /* set some typical fit error by default */
    cpl_error_code errfit = CPL_ERROR_SINGULAR_MATRIX;

    if (aDLambdas) { /* use our own polynomial fitting with weighting */
      /* the lvmq function wants N rows of dimension D=2 *
       * instead of the other way around...              */
      cpl_matrix *xypos = cpl_matrix_transpose_create(aXYPos);

      int npar = 2 + (aParams->xorder + 1) * (aParams->yorder + 1);
      cpl_vector *pars = cpl_vector_new(npar);
      /* pre-fill all but the zero-order with zero as the first guess value */
      cpl_vector_fill(pars, 0.);
      /* the polynomial orders go first into the array */
      cpl_vector_set(pars, 0, aParams->xorder);
      cpl_vector_set(pars, 1, aParams->yorder);
      cpl_vector_set(pars, 2, 5000.); /* near lower edge of lambda range */
      cpl_array *aflags = cpl_array_new(npar, CPL_TYPE_INT);
      /* most are real (free == 1) parameters, except the first two */
      cpl_array_set_int(aflags, 0, 0); /* fixed xorder */
      cpl_array_set_int(aflags, 1, 0); /* fixed yorder */
      cpl_array_fill_window_int(aflags, 2, npar - 2, 1);
      int *pflags = cpl_array_get_data_int(aflags);
      errfit = cpl_fit_lvmq(xypos, /* sigma_x (unsupported) */NULL,
                            aLambdas, aDLambdas, pars, pflags,
                            muse_wave_poly_2d, muse_wave_dpoly_2d,
                            CPL_FIT_LVMQ_TOLERANCE, CPL_FIT_LVMQ_COUNT,
                            CPL_FIT_LVMQ_MAXITER, NULL, NULL, NULL);
      cpl_array_delete(aflags);
      cpl_size k = 2; /* coefficients start at the 3rd vector entry */
      unsigned short i;
      for (i = 0; i <= aParams->xorder; i++) {
        unsigned short j;
        for (j = 0; j <= aParams->yorder; j++) {
          cpl_size pows[2] = { i, j }; /* trick to access the polynomial */
          cpl_polynomial_set_coeff(*aPoly, pows,
                                   cpl_vector_get(pars, k++));
        } /* for j */
      } /* for i */
      cpl_matrix_delete(xypos);
      cpl_vector_delete(pars);
    } else {
      /* use CPL polynomial fitting for everything else */
      const cpl_boolean sym = CPL_FALSE;
      const cpl_size mindeg2d[] = { 0, 0 },
                     maxdeg2d[] = { aParams->xorder, aParams->yorder };
      errfit = cpl_polynomial_fit(*aPoly, aXYPos, &sym, aLambdas, NULL,
                                  CPL_TRUE, mindeg2d, maxdeg2d);
    } /* CPL polynomial fitting */
#if 0
    printf("polynomial (orders=%hu/%hu, degree=%"CPL_SIZE_FORMAT"):\n",
           aParams->xorder, aParams->yorder, cpl_polynomial_get_degree(*aPoly));
    cpl_polynomial_dump(*aPoly, stdout);
    fflush(stdout);
#endif

    if (errfit) {
      /* the fit failed, complain and abort */
      cpl_msg_error(__func__, "The polynomial fit in slice %hu failed: %s",
                    aSlice, cpl_error_get_message());

#if 0
      /* more fancy plotting of input datapoints *
       * (color coded lambda over x/y-position)  */
      FILE *gp = popen("gnuplot -persist", "w");
      if (gp) {
        /* plot title with fit details */
        fprintf(gp, "set title \"2D polynomial fit residuals (failed fit: "
                "\'%s\')\n", cpl_error_get_message());
        /* set nice palette */
        fprintf(gp, "set palette defined ( 0 \"dark-violet\","
                    "1 \"dark-blue\", 4 \"green\", 6 \"yellow\", 8 \"orange\","
                    "9 \"red\", 10 \"dark-red\")\n");
        fprintf(gp, "unset key\n");
        cpl_matrix *xpos = cpl_matrix_extract_row(aXYPos, 0);
        fprintf(gp, "set xrange [%f:%f]\n", cpl_matrix_get_min(xpos),
                cpl_matrix_get_max(xpos));
        cpl_matrix_delete(xpos);
        fprintf(gp, "set cbrange [%f:%f]\n", cpl_vector_get_min(aLambdas),
                cpl_vector_get_max(aLambdas));
        fprintf(gp, "plot \"-\" w p pal\n");

        printf("# X Y lambda\n");
        int n;
        for (n = 0; n < cpl_vector_get_size(aLambdas); n++) {
          printf("%4d %7.2f  %8.3f\n", (int)cpl_matrix_get(aXYPos, 0, n),
                  cpl_matrix_get(aXYPos, 1, n), cpl_vector_get(aLambdas, n));
          fprintf(gp, "%f %f %f\n", cpl_matrix_get(aXYPos, 0, n),
                  cpl_matrix_get(aXYPos, 1, n), cpl_vector_get(aLambdas, n));
        }
        fflush(stdout);
        fprintf(gp, "EOF\n");
        pclose(gp);
      }
#endif

      /* make sure we delete the polynomial */
      cpl_polynomial_delete(*aPoly);
      *aPoly = NULL; /* make sure we can detect the failure */
      return CPL_ERROR_ILLEGAL_OUTPUT;
    } /* if failed fit */

    int npoints = cpl_vector_get_size(aLambdas);
    cpl_vector *res = cpl_vector_new(npoints);
    /* the aDLambdas parameter (the errors of the datapoints) do not *
     * change the filled vector, just the chi**2 which we don't need */
    cpl_vector_fill_polynomial_fit_residual(res, aLambdas, aDLambdas, *aPoly,
                                            aXYPos, NULL);
    /* compute the mean-squared error, without any weighting, in any case */
    double mse = 0;
    /* compute a (weighted) mean-squared error, if we know the error bars */
    if (aDLambdas) { /* no errors given, equal weights for all points */
      double wsum = 0;
      int i;
      for (i = 0; i < npoints; i++) {
        double weight = 1. / cpl_vector_get(aDLambdas, i);
        mse += pow(cpl_vector_get(res, i), 2) * weight;
        wsum += weight;
      } /* for i */
      mse /= wsum;
    } else {
      mse = cpl_vector_product(res, res) / npoints;
    }
    double rlimit = rsigma * sqrt(mse); /* limit using possibly weighted RMS */
    if (debug) {
      printf("Resulting 2D polynomial of slice %hu (%d points, RMS = %f dRMS = "
             "%f), %.1f-sigma limit now %f (target RMS = %f):\n", aSlice,
             npoints, sqrt(mse), rms < 0 ? 0. : rms - sqrt(mse), rsigma, rlimit,
             aParams->targetrms);
      if (debug > 1) {
        cpl_polynomial_dump(*aPoly, stdout);
      }
      fflush(stdout);
    }
#if 0
    /* simple plotting of intermediate residuals   *
     * (value over arbitrary x-position in vector) */
    cpl_plot_vector("set title \"res\"\n", "", "", res);
#endif
    if (aParams->rflag) { /* dump debug information into table, if requested */
      int nnew = cpl_vector_get_size(res),
          nrow = aParams->residuals ? cpl_table_get_nrow(aParams->residuals) : 0;
      /* create the residuals table if wanted and not yet existing */
      if (!aParams->residuals) {
        aParams->residuals = muse_cpltable_new(muse_wavedebug_def, nnew);
        cpl_table_set_column_savetype(aParams->residuals, "slice", CPL_TYPE_UCHAR);
      } else { /* resize table as needed to fill in the new entries */
        cpl_table_set_size(aParams->residuals, nrow + nnew);
      }
      cpl_table_fill_column_window_int(aParams->residuals, "slice",
                                       nrow, nnew, aSlice);
      cpl_table_fill_column_window_int(aParams->residuals, "iteration",
                                       nrow, nnew, niter);
      cpl_table_fill_column_window_double(aParams->residuals, "rejlimit",
                                          nrow, nnew, rlimit);
      int n;
      for (n = 0; n < cpl_vector_get_size(res); n++) {
        cpl_table_set_int(aParams->residuals, "x", nrow + n,
                          (int)cpl_matrix_get(aXYPos, 0, n));
        cpl_table_set_float(aParams->residuals, "y", nrow + n,
                            cpl_matrix_get(aXYPos, 1, n));
        cpl_table_set_float(aParams->residuals, "lambda", nrow + n,
                            cpl_vector_get(aLambdas, n));
        cpl_table_set_double(aParams->residuals, "residual", nrow + n,
                             cpl_vector_get(res, n));
      } /* for n (all wavelengths) */
      if (debug) {
        cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" entries in residuals table "
                      "after iteration %d of slice %hu",
                      cpl_table_get_nrow(aParams->residuals), niter, aSlice);
      }
    } /* if debug table */
    niter++;

    /* if the fit is already good enough, we don't *
     * need to go through point rejection any more */
    cpl_boolean isgoodenough = (rms < 0 ? CPL_FALSE : rms - sqrt(mse) < 0.001)
                             || (sqrt(mse) <= aParams->targetrms);
    large_residuals = 0;
    int i;
    for (i = 0; !isgoodenough && i < cpl_vector_get_size(res); i++) {
      /* compare this residual value to the RMS value */
      if (fabs(cpl_vector_get(res, i)) < rlimit) {
        /* good fit at this point */
        continue;
      }
      /* bad residual, remove element, from residuals vector and the data vectors */
#if 0
      cpl_msg_debug(__func__, "residual = %f (position %fx%f, lambda=%f+/-%f)",
                    cpl_vector_get(res, i),
                    cpl_matrix_get(aXYPos, 0, i), cpl_matrix_get(aXYPos, 1, i),
                    cpl_vector_get(aLambdas, i),
                    aDLambdas ? cpl_vector_get(aDLambdas, i) : 1.);
#endif
      if (cpl_vector_get_size(res) == 1) {
        cpl_msg_debug(__func__, "trying to remove the last vector/matrix "
                      "element when checking against fit sigma (slice %hu)",
                      aSlice);
        break;
      }
      /* remove bad element from the fit structures... */
      muse_cplvector_erase_element(res, i);
      cpl_matrix_erase_columns(aXYPos, i, 1);
      muse_cplvector_erase_element(aLambdas, i);
      if (aDLambdas) {
        muse_cplvector_erase_element(aDLambdas, i);
      }

      large_residuals++;
      i--; /* we stay at this position to see what moved here */
    } /* for i */

    rms = sqrt(mse);
    if (!large_residuals) {
      /* no large residuals (any more), so this is the final solution */

      /* save the mean squared error */
      if (aMSE) {
        *aMSE = mse;
      }
    } /* if !large_residuals */

    cpl_vector_delete(res);
  } /* while large_residuals */

  return CPL_ERROR_NONE;
} /* muse_wave_poly_fit() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Create the table to save te wave wavelength calibration coefficients.
  @param   aNSlices    the number of slices to be stored in this table
  @param   aXOrder     the polynomial order in x-direction
  @param   aYOrder     the polynomial order in y-direction
  @return  the newly created table on success, NULL on error

  @remark the returned table has to be deallocated using cpl_table_delete().
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wave_table_create(const unsigned short aNSlices, const unsigned short aXOrder,
                       const unsigned short aYOrder)
{
  cpl_table *table = cpl_table_new(aNSlices);
  cpl_ensure(table, CPL_ERROR_UNSPECIFIED, NULL);

  /* create column for slice number*/
  cpl_table_new_column(table, MUSE_WAVECAL_TABLE_COL_SLICE_NO, CPL_TYPE_INT);
  cpl_table_set_column_unit(table, MUSE_WAVECAL_TABLE_COL_SLICE_NO, "No");
  cpl_table_set_column_format(table, MUSE_WAVECAL_TABLE_COL_SLICE_NO, "%2d");

  /* create columns for all coefficients, so loop over fit orders */
  unsigned short i;
  for (i = 0; i <= aXOrder; i++) {
    unsigned short j;
    for (j = 0; j <= aYOrder; j++) {
      /* create column name, start coefficient names at 0 */
      char *colname = cpl_sprintf(MUSE_WAVECAL_TABLE_COL_COEFF, i, j);
      cpl_table_new_column(table, colname, CPL_TYPE_DOUBLE);
      /* fit coeff are in wavelength space */
      cpl_table_set_column_unit(table, colname, "Angstrom");
      cpl_table_set_column_format(table, colname, "%12.5e");
      cpl_free(colname);
    }
  }
  /* create column for mean squared error of fitting parameters */
  cpl_table_new_column(table, MUSE_WAVECAL_TABLE_COL_MSE, CPL_TYPE_DOUBLE);
  cpl_table_set_column_format(table, MUSE_WAVECAL_TABLE_COL_MSE, "%f");

  return table;
} /* muse_wave_table_create() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Save the given polynomials to the wavelength calibration table.
  @param   aTable    table to save the solution into
  @param   aPoly     the wavelength solution polynomial
  @param   aMSE      the mean squared error for the solution
  @param   aXOrder   the polynomial order in x-direction
  @param   aYOrder   the polynomial order in y-direction
  @param   aRow      table row to use (slice index)
  @return  CPL_ERROR_NONE on success, a cpl_error_code on failure

  @error{return CPL_ERROR_NULL_INPUT, aTable or aPoly are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, dimension of aPoly is not 2}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_table_add_poly(cpl_table *aTable, cpl_polynomial *aPoly,
                         double aMSE, unsigned short aXOrder,
                         unsigned short aYOrder, const unsigned short aRow)
{
  cpl_ensure_code(aTable && aPoly, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(cpl_polynomial_get_dimension(aPoly) == 2,
                  CPL_ERROR_ILLEGAL_INPUT);

  /* row numbers start at 0 but the numbers in the table should start with 1 */
  cpl_table_set_int(aTable, MUSE_WAVECAL_TABLE_COL_SLICE_NO, aRow, aRow + 1);
  cpl_table_set_double(aTable, MUSE_WAVECAL_TABLE_COL_MSE, aRow, aMSE);

  /* i and j loop over all orders of the polynomial */
  unsigned short i;
  for (i = 0; i <= aXOrder; i++) {
    unsigned short j;
    for (j = 0; j <= aYOrder; j++) {
      cpl_size pows[2] = { i, j }; /* trick to access the polynomial */

      char *colname = cpl_sprintf(MUSE_WAVECAL_TABLE_COL_COEFF, i, j);
      cpl_error_code rc = cpl_table_set_double(aTable, colname, aRow,
                                               cpl_polynomial_get_coeff(aPoly,
                                                                        pows));
      if (rc != CPL_ERROR_NONE) {
        cpl_msg_warning(__func__, "Problem writing %f to field %s in "
                        "wavelength table: %s",
                        cpl_polynomial_get_coeff(aPoly, pows), colname,
                        cpl_error_get_message());
        cpl_polynomial_dump(aPoly, stdout);
        cpl_table_dump(aTable, aRow, 1, stdout);
        fflush(stdout);
      }
      cpl_free(colname);
    } /* for j (polynomial orders in y) */
  } /* for i (polynomial orders in x) */

  return CPL_ERROR_NONE;
} /* muse_wave_table_add_poly() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Determine the x- and y-order of the polynomial stored in a wavelength
           calibration table.
  @param   aWave       the table containing the wavelength calibration solution
  @param   aXOrder     the pointer to the x-order to return
  @param   aYOrder     the pointer to the y-order to return
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  This function assumes that the second last column in the input table contains
  the highest order and that its name is of the format wlcXY where X and Y are
  the numbers that we want to find out here.

  @error{return CPL_ERROR_NULL_INPUT, aWave\, aXOrder\, or aYOrder are NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_table_get_orders(const cpl_table *aWave, unsigned short *aXOrder,
                           unsigned short *aYOrder)
{
  cpl_ensure_code(aWave && aXOrder && aYOrder, CPL_ERROR_NULL_INPUT);
  cpl_array *cols = cpl_table_get_column_names(aWave);

  /* second last entry will contain highest order (last is MSE) */
  const char *highcol = cpl_array_get_string(cols, cpl_array_get_size(cols) - 2);
  char *colname = cpl_strdup(highcol); /* duplicate it so that we can manipulate it */
  cpl_array_delete(cols);

  /* we can directly get the value of the last digit */
  *aYOrder = atoi(colname+4);
  /* now null out the last digit so that we can get the first one only */
  colname[4] = '\0';
  *aXOrder = atoi(colname+3);
  cpl_free(colname);

  return CPL_ERROR_NONE;
} /* muse_wave_table_get_orders() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Construct polynomial from the wavelength calibration table entry for
           the given slice.
  @param   aTable   wavelength calibration table holding the waveelength
                    calibration solution
  @param   aSlice   the slice number, between 1 and kMuseSlicesPerCCD
  @return  the polynomial defining the wavelength solution of the given slice
           or NULL on error

  The returned polynomial has to be deallocated using cpl_polynomial_delete()
  after use.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aTable is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aSlice is outside the valid range of slices for MUSE}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         the requested slice cannot be found in the table}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return NULL,
         a coefficient in aTable cannot be read}
 */
/*----------------------------------------------------------------------------*/
cpl_polynomial *
muse_wave_table_get_poly_for_slice(const cpl_table *aTable,
                                   const unsigned short aSlice)
{
  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aSlice >= 1 && aSlice <= kMuseSlicesPerCCD,
             CPL_ERROR_ILLEGAL_INPUT, NULL);
  /* search for row containing the requested slice, first to access it *
   * in a possibly incomplete table, and second to check its presence! */
  int irow, nrow = cpl_table_get_nrow(aTable);
  for (irow = 0; irow < nrow; irow++) {
    int err;
    unsigned short slice = cpl_table_get_int(aTable,
                                             MUSE_WAVECAL_TABLE_COL_SLICE_NO,
                                             irow, &err);
    if (slice == aSlice && !err) {
      break;
    }
  } /* for irow */
  cpl_ensure(irow < nrow, CPL_ERROR_DATA_NOT_FOUND, NULL);

  cpl_polynomial *pwave = cpl_polynomial_new(2);
  char colname[15]; /* "wlc" plus max 2x5 chars for the numbers */
  unsigned short wavexorder, waveyorder;
  muse_wave_table_get_orders(aTable, &wavexorder, &waveyorder);
  unsigned short l;
  for (l = 0; l <= wavexorder; l++) {
    unsigned short k;
    for (k = 0; k <= waveyorder; k++) {
      cpl_size pows[2] = { l, k }; /* trick to access the polynomial */
      sprintf(colname, MUSE_WAVECAL_TABLE_COL_COEFF, l, k);
      int err;
      cpl_polynomial_set_coeff(pwave, pows,
                               cpl_table_get_double(aTable, colname, irow,
                                                    &err));
      if (err != 0) { /* broken table entry */
        cpl_polynomial_delete(pwave);
        cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT, "Wavelength "
                              "calibration table broken in slice %hu (row index"
                              " %d) column %s", aSlice, irow, colname);
        return NULL;
      } /* if */
    } /* for k */
  } /* for l */
  return pwave;
} /* muse_wave_table_get_poly_for_slice() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Write out a wavelength map for visual checks.
  @param   aImage      image that used to contruct the map
  @param   aWave       the table containing the wavelength calibration solution
  @param   aTrace      the table containing the tracing solution
  @return  a cpl_image * containing the wavelength map or NULL on error
  @remark  A wavelength map is an image in which the value of each pixel is the
           wavelength that is assigned to it by the wavelength calibration.
           It can be useful during debugging.
  @remark  Only the dimensions of the data component of aImage are used (for the
           construction of the output map). Its header, dq, and stat extensions
           do not need to exist.

  Loop through all pixels in the image, evaluate, if the pixel is within a slice.
  If so, evaluate the respective wavelength solution and set the pixel value in
  the output image to the wavelength derived.

  @todo This function shares a lot of code with muse_pixtable_create().

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         the input image\, the input wavelength calibration solution table\, or the input trace solution table are NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_image *
muse_wave_map(muse_image *aImage, const cpl_table *aWave,
              const cpl_table *aTrace)
{
  cpl_ensure(aImage && aWave && aTrace, CPL_ERROR_NULL_INPUT, NULL);
  int nx = cpl_image_get_size_x(aImage->data),
      ny = cpl_image_get_size_y(aImage->data);
  cpl_image *wavemap = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
  cpl_ensure(wavemap, cpl_error_get_code(), NULL);
  unsigned char ifu = muse_utils_get_ifu(aImage->header);

  /* get output image buffer as pointer */
  float *wdata = cpl_image_get_data_float(wavemap);

  unsigned short wavexorder, waveyorder;
  muse_wave_table_get_orders(aWave, &wavexorder, &waveyorder);
  cpl_msg_debug(__func__, "Order for trace solution is %d, for wavelength "
                "solution %hu/%hu, IFU %hhu", muse_trace_table_get_order(aTrace),
                wavexorder, waveyorder, ifu);

  /* loop through all slices */
  unsigned short islice;
  for (islice = 0; islice < kMuseSlicesPerCCD; islice++) {
#if 0
    cpl_msg_debug(__func__, "Starting to process slice %d of IFU %hhu",
                  (int)islice + 1, ifu);
#endif

    /* fill the wavelength calibration polynomial for this slice */
    cpl_polynomial *pwave = muse_wave_table_get_poly_for_slice(aWave, islice + 1);
    /* vector for the position within the slice (for evaluation *
     * of the wavelength solution in two dimensions             */
    cpl_vector *pos = cpl_vector_new(2);

    /* get the tracing polynomials for this slice */
    cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
                                                                   islice + 1);
    if (!ptrace) {
      cpl_msg_warning(__func__, "slice %2d of IFU %hhu: tracing polynomials "
                      "missing!", (int)islice + 1, ifu);
      continue;
    }
#if 0
    printf("polynomials for slice %d:\n", (int)islice + 1);
    cpl_polynomial_dump(ptrace[MUSE_TRACE_LEFT], stdout);
    cpl_polynomial_dump(ptrace[MUSE_TRACE_RIGHT], stdout);
    cpl_polynomial_dump(pwave, stdout);
    fflush(stdout);
#endif

    /* within each slice, loop from bottom to top */
    int j;
    for (j = 1; j <= ny; j++) {
      /* determine the slice edges for this vertical    *
       * position so that we can loop over those pixels */
      int ileft = ceil(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT],
                                              j, NULL)),
          iright = floor(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT],
                                                j, NULL));
      /* try to detect faulty polynomials */
      if (ileft < 1 || iright > nx || ileft > iright) {
        cpl_msg_warning(__func__, "slice %2d of IFU %hhu: faulty polynomial "
                        "detected at y=%d", (int)islice + 1, ifu, j);
        break; /* skip the rest of this slice */
      }
      cpl_vector_set(pos, 1, j); /* vertical pos. for wavelength evaluation */

      /* now loop over all pixels of this slice horizontally */
      int i;
      for (i = ileft; i <= iright; i++) {
        cpl_vector_set(pos, 0, i); /* horiz. pos. for wavelength evaluation */

        /* compute the wavelength of this pixel */
        wdata[(i-1) + (j-1)*nx] = cpl_polynomial_eval(pwave, pos);
      } /* for i (horizontal pixels) */
    } /* for j (vertical direction) */

    /* we are now done with this slice, clean up */
    muse_trace_polys_delete(ptrace);
    cpl_polynomial_delete(pwave);
    cpl_vector_delete(pos);
  } /* for islice */

  return wavemap;
} /* muse_wave_map() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Fancy plotting of wavelength calibration residuals (color coded over
           x/y-position) using gnuplot.
  @param   aTable        the table with debug information on wavelength
                         calibration residuals
  @param   aIFU          the IFU number (only used if > 0)
  @param   aSlice        slice number to plot (0 for all)
  @param   aIter         iteration to plot (0 for the last one)
  @param   aPlotLambda   if true, plot lambda values on the y axis
  @param   aCuts         if not NULL and of size 2, use contents as color cuts
  @return  CPL_ERROR_NONE on success a CPL error code on failure

  @note This function directly works on the input table data, i.e. erases
        all rows that are not relevant!

  @error{return CPL_ERROR_ILLEGAL_INPUT,
         input table is not a MUSE wavelength calibration residuals table}
  @error{return CPL_ERROR_ASSIGNING_STREAM,
         gnuplot stream (pipe) could not be opened}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         platform does not have popen() or pclose()}
  @error{return CPL_ERROR_DATA_NOT_FOUND,
         data for this slice/iteration was not found in the input table}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_plot_residuals(cpl_table *aTable, unsigned char aIFU,
                         unsigned short aSlice, unsigned int aIter,
                         cpl_boolean aPlotLambda, cpl_vector *aCuts)
{
#if HAVE_POPEN && HAVE_PCLOSE
  cpl_ensure_code(aTable, CPL_ERROR_NULL_INPUT);
  cpl_error_code rc = muse_cpltable_check(aTable, muse_wavedebug_def);
  cpl_ensure_code(rc == CPL_ERROR_NONE, rc);

  FILE *gp = popen("gnuplot", "w");
  if (!gp) {
    return CPL_ERROR_ASSIGNING_STREAM;
  }

  /* select relevant rows in the table */
  cpl_table_unselect_all(aTable); /* start clean */

  int n, nrow = cpl_table_get_nrow(aTable),
      error = 0;
  if (aSlice == 0) {
    printf("Selecting data of all slices");
    if (aIFU > 0) {
      printf(" of IFU %hhu", aIFU);
    }
    printf(".\n");

    const int *slice = cpl_table_get_data_int_const(aTable, "slice"),
              *iter = cpl_table_get_data_int_const(aTable, "iteration");
    if (!aIter) { /* last iteration */
      fprintf(stderr, "Selecting data of last iteration of all slices\n");
      /* the last row of each slice number is the one with the last iteration */
      int sliceno = slice[nrow - 1],
          iterlast = iter[nrow - 1];
      for (n = nrow - 2; n >= 0; n--) {
        if (slice[n] == sliceno && iter[n] != iterlast) {
          cpl_table_select_row(aTable, n);
        }
        if (slice[n] != sliceno) { /* previous slice, reset comparison */
          sliceno = slice[n];
          iterlast = iter[n];
        }
      } /* for n (table rows) */
      cpl_table_erase_selected(aTable);
      /* plot title with some details */
      fprintf(gp, "set title \"");
      if (aIFU > 0) {
        fprintf(gp, "IFU %hhu, ", aIFU);
      }
      fprintf(gp, "slices %d..%d, iterations %d..%d: 2D polynomial fit "
              "residuals (limits %f..%f)\n",
              (int)cpl_table_get_column_min(aTable, "slice"),
              (int)cpl_table_get_column_max(aTable, "slice"),
              (int)cpl_table_get_column_min(aTable, "iteration"),
              (int)cpl_table_get_column_max(aTable, "iteration"),
              cpl_table_get_column_min(aTable, "rejlimit"),
              cpl_table_get_column_max(aTable, "rejlimit"));
    } else {
      printf("Selecting data of iteration %d.\n", aIter);
      for (n = 0; n < nrow; n++) {
        if (iter[n] != (int)aIter) {
          cpl_table_select_row(aTable, n);
        }
      } /* for n (table rows) */
      cpl_table_erase_selected(aTable);
      /* plot title with some details */
      fprintf(gp, "set title \"");
      if (aIFU > 0) {
        fprintf(gp, "IFU %hhu, ", aIFU);
      }
      fprintf(gp, "slices %d..%d, iteration %d: 2D polynomial fit residuals "
              "(limits %f..%f)\n",
              (int)cpl_table_get_column_min(aTable, "slice"),
              (int)cpl_table_get_column_max(aTable, "slice"), aIter,
              cpl_table_get_column_min(aTable, "rejlimit"),
              cpl_table_get_column_max(aTable, "rejlimit"));
    }
  } else {
    printf("Selecting data of ");
    if (aIFU > 0) {
      printf("IFU %hhu ", aIFU);
    }
    printf("slice %hu.\n", aSlice);
    const int *slice = cpl_table_get_data_int_const(aTable, "slice");
    for (n = 0; n < nrow; n++) {
      if (slice[n] != aSlice) {
        cpl_table_select_row(aTable, n);
      }
    } /* for n (table rows) */
    cpl_table_erase_selected(aTable);
    nrow = cpl_table_get_nrow(aTable);
    cpl_table_unselect_all(aTable);

    const int *iter = cpl_table_get_data_int_const(aTable, "iteration");
    if (!aIter) { /* last iteration */
      /* table is sorted, so last iteration is that of the last row */
      aIter = iter[nrow - 1];
    }
    printf("Selecting data of iteration %d.\n", aIter);
    for (n = 0; n < nrow; n++) {
      if (iter[n] != (int)aIter) {
        cpl_table_select_row(aTable, n);
      }
    } /* for n (table rows) */
    cpl_table_erase_selected(aTable);

    /* plot title with some details */
    fprintf(gp, "set title \"");
    if (aIFU > 0) {
      fprintf(gp, "IFU %hhu, ", aIFU);
    }
    fprintf(gp, "slice %hu, iteration %d: 2D polynomial fit residuals "
            "(limit=%f)\n", aSlice, aIter,
            cpl_table_get_double(aTable, "rejlimit", 0, &error));
  } /* single slice */

  /* data access */
  nrow = cpl_table_get_nrow(aTable);
  cpl_ensure_code(nrow > 0, CPL_ERROR_DATA_NOT_FOUND);
  printf("Plotting %d points.\n", nrow);
  const int *x = cpl_table_get_data_int_const(aTable, "x");
  const float *y = cpl_table_get_data_float_const(aTable, "y"),
              *lambda = cpl_table_get_data_float_const(aTable, "lambda");
  const double *r = cpl_table_get_data_double_const(aTable, "residual");
  /*           *rlimit = cpl_table_get_data_double_const(aTable, "rejlimit"); */

  /* determine plotting limits */
  int xmin = cpl_table_get_column_min(aTable, "x") - 2,
      xmax = cpl_table_get_column_max(aTable, "x") + 2;
  float ymin = cpl_table_get_column_min(aTable, "y") - 2,
        ymax = cpl_table_get_column_max(aTable, "y") + 2,
        lmin = cpl_table_get_column_min(aTable, "lambda") - 2,
        lmax = cpl_table_get_column_max(aTable, "lambda") + 2;
  double rmin = cpl_table_get_column_min(aTable, "residual"),
         rmax = cpl_table_get_column_max(aTable, "residual");
  if (aCuts && cpl_vector_get_size(aCuts) == 2) {
    rmin = cpl_vector_get(aCuts, 0);
    rmax = cpl_vector_get(aCuts, 1);
  }

  /* set nice palette */
  fprintf(gp, "set palette defined ( 0 \"dark-violet\","
              "1 \"dark-blue\", 4 \"green\", 6 \"yellow\", 8 \"orange\","
              "9 \"red\", 10 \"dark-red\")\n");
  fprintf(gp, "unset key\n");
  printf("Setting plotting limits: [%d:%d][%.2f:%.2f][%.4f:%.4f]\n",
         xmin, xmax, aPlotLambda ? lmin : ymin, aPlotLambda ? lmax : ymax,
         rmin, rmax);
  fprintf(gp, "set xrange [%d:%d]\n", xmin, xmax);
  if (aPlotLambda) {
    fprintf(gp, "set yrange [%f:%f]\n", lmin, lmax);
  } else {
    fprintf(gp, "set yrange [%f:%f]\n", ymin, ymax);
  }
  fprintf(gp, "set cbrange [%f:%f]\n", rmin, rmax);
  fprintf(gp, "set view map\n");
  fprintf(gp, "splot \"-\" w p pal\n");
  for (n = 0; n < nrow; n++) {
    if (aPlotLambda) {
      fprintf(gp, "%d %f %e\n", x[n], lambda[n], r[n]);
    } else {
      fprintf(gp, "%d %f %e\n", x[n], y[n], r[n]);
    }
  }
  fprintf(gp, "EOF\n");
  fflush(gp);
  /* request keypress, so that working with the mouse keeps   *
   * working and gnuplot has enough time to actually draw all *
   * the stuff before the files get removed                   */
  printf("Press ENTER to end program and close plot\n");
  getchar();
  pclose(gp);
  return CPL_ERROR_NONE;
#else /* no HAVE_POPEN && HAVE_PCLOSE */
  return CPL_ERROR_UNSUPPORTED_MODE;
#endif /* HAVE_POPEN && HAVE_PCLOSE */
} /* muse_wave_plot_residuals() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Plot wavelength calibration polynomial and data or residuals using
           gnuplot.
  @param   aCTable    the table with the wavelength solution polynomials
  @param   aRTable    the table with debug information on wavelength
                      calibration residuals
  @param   aIFU       the IFU number (only used if > 0)
  @param   aSlice     slice number to plot
  @param   aIter      iteration to plot (0 for the last one)
  @param   aColumn    CCD column to plot (0 for the central one of the slice)
  @param   aPlotRes   if true, plot lambda values on the y axis
  @return  CPL_ERROR_NONE on success a CPL error code on failure

  @note This function directly works on the input table data, i.e. erases
        all rows that are not relevant!

  @error{return CPL_ERROR_NULL_INPUT, one of the input tables is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, one of the input tables is faulty}
  @error{return CPL_ERROR_ACCESS_OUT_OF_RANGE, slice number is invalid}
  @error{return CPL_ERROR_ASSIGNING_STREAM,
         gnuplot stream (pipe) could not be opened}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         platform does not have popen() or pclose()}
  @error{return CPL_ERROR_DATA_NOT_FOUND,
         data for this slice/iteration was not found in the input table}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wave_plot_column(cpl_table *aCTable, cpl_table *aRTable,
                      unsigned char aIFU, unsigned short aSlice,
                      unsigned int aColumn, unsigned int aIter,
                      cpl_boolean aPlotRes)
{
#if HAVE_POPEN && HAVE_PCLOSE
  cpl_ensure_code(aCTable && aRTable, CPL_ERROR_NULL_INPUT);
  cpl_error_code rc = muse_cpltable_check(aRTable, muse_wavedebug_def);
  cpl_ensure_code(rc == CPL_ERROR_NONE, rc);
  /* WAVECAL_TABLE doesn't has a _def structure to check, so test orders */
  unsigned short xorder, yorder;
  muse_wave_table_get_orders(aCTable, &xorder, &yorder);
  cpl_ensure_code(xorder > 0 && yorder > 0, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(aSlice >= 1 && aSlice <= kMuseSlicesPerCCD,
                  CPL_ERROR_ACCESS_OUT_OF_RANGE);

  FILE *gp = popen("gnuplot", "w");
  if (!gp) {
    return CPL_ERROR_ASSIGNING_STREAM;
  }

  /* select relevant rows in the table */
  cpl_table_unselect_all(aRTable); /* start clean */

  printf("Selecting data of ");
  if (aIFU > 0) {
    printf("IFU %hhu ", aIFU);
  }
  printf("slice %hu.\n", aSlice);
  const int *slice = cpl_table_get_data_int_const(aRTable, "slice");
  int n, nrow = cpl_table_get_nrow(aRTable);
  for (n = 0; n < nrow; n++) {
    if (slice[n] != aSlice) {
      cpl_table_select_row(aRTable, n);
    }
  } /* for n (table rows) */
  cpl_table_erase_selected(aRTable);
  nrow = cpl_table_get_nrow(aRTable);
  cpl_ensure_code(nrow > 0, CPL_ERROR_DATA_NOT_FOUND);
  cpl_table_unselect_all(aRTable);

  const int *iter = cpl_table_get_data_int_const(aRTable, "iteration");
  if (!aIter) { /* last iteration */
    /* table is sorted, so last iteration is that of the last row */
    aIter = iter[nrow - 1];
  }
  printf("Selecting data of iteration %d.\n", aIter);
  for (n = 0; n < nrow; n++) {
    if (iter[n] != (int)aIter) {
      cpl_table_select_row(aRTable, n);
    }
  } /* for n (table rows) */
  cpl_table_erase_selected(aRTable);
  nrow = cpl_table_get_nrow(aRTable);
  cpl_ensure_code(nrow > 0, CPL_ERROR_DATA_NOT_FOUND);
  cpl_table_unselect_all(aRTable);

  unsigned int col1 = cpl_table_get_column_min(aRTable, "x"),
               col2 = cpl_table_get_column_max(aRTable, "x");
  if (aColumn > 0) {
    col1 = col2 = aColumn;
  } else if (aColumn > (unsigned)kMuseOutputXRight) {
    /* approximate central column of the slice */
    col1 = col2 = (col1 + col2) / 2;
  }
  printf("Plotting data of columns %u..%u.\n", col1, col2);

  /* determine plotting limits */
  float ymin = cpl_table_get_column_min(aRTable, "y") - 10,
        ymax = cpl_table_get_column_max(aRTable, "y") + 10,
        lmin = cpl_table_get_column_min(aRTable, "lambda") - 10,
        lmax = cpl_table_get_column_max(aRTable, "lambda") + 10;
  double rmin = cpl_table_get_column_min(aRTable, "residual") * 1.03,
         rmax = cpl_table_get_column_max(aRTable, "residual") * 1.03;
  /* plot title with some details */
  fprintf(gp, "set title \"");
  if (aIFU > 0) {
    fprintf(gp, "IFU %hhu, ", aIFU);
  }
  fprintf(gp, "slice %hu, iteration %d, column %u..%u: polynomial and ", aSlice,
          aIter, col1, col2);
  printf("Setting plotting limits: ");
  if (aPlotRes) {
    fprintf(gp, "residuals (limit=%f)\"\n",
            cpl_table_get_double(aRTable, "rejlimit", 0, NULL));
    printf("[%.2f:%.2f][%.4f:%.4f]\n", lmin, lmax, rmin, rmax);
    fprintf(gp, "set xrange [%f:%f]\n", lmin, lmax);
    fprintf(gp, "set yrange [%f:%f]\n", rmin, rmax);
    fprintf(gp, "set xlabel \"Wavelength [Angstrom]\"\n");
    fprintf(gp, "set ylabel \"Residuals [Angstrom]\"\n");
  } else {
    fprintf(gp, "arc line positions\"\n");
    printf("[%.2f:%.2f][%.2f:%.2f]\n", ymin, ymax, lmin, lmax);
    fprintf(gp, "set xrange [%g:%g]\n", ymin, ymax);
    fprintf(gp, "set yrange [%f:%f]\n", lmin, lmax);
    fprintf(gp, "set xlabel \"y-position [pix]\"\n");
    fprintf(gp, "set ylabel \"Wavelength [Angstrom]\"\n");
  }
  fprintf(gp, "set key outside below\n");
  fprintf(gp, "set samples 1000\n"); /* more samples so that one can zoom in */

  /* create gnuplot polynomial from table coefficients */
  fprintf(gp, "p(x,y) = 0 ");
  if (!aPlotRes) { /* create full polynomial */
    unsigned short i;
    for (i = 0; i <= xorder; i++) {
      unsigned short j;
      for (j = 0; j <= yorder; j++) {
        char *coeff = cpl_sprintf(MUSE_WAVECAL_TABLE_COL_COEFF, i, j);
        double cvalue = cpl_table_get(aCTable, coeff, aSlice - 1, NULL);
        cpl_free(coeff);
        fprintf(gp, " + (%g) * x**(%hu) * y**(%hu)", cvalue, i, j);
      } /* for j (y orders) */
    } /* for i (x orders) */
  }
  fprintf(gp, "\n");

  const int *x = cpl_table_get_data_int_const(aRTable, "x");
  const float *y = cpl_table_get_data_float_const(aRTable, "y"),
              *lambda = cpl_table_get_data_float_const(aRTable, "lambda");
  const double *r = cpl_table_get_data_double_const(aRTable, "residual");

  /* distribute columns over 256 color values */
  double dcol = (col2 - col1) / 255.;
  if (dcol == 0.) { /* take care not to produce NANs below when dividing */
    dcol = 1.;
  }
  /* construct the plot command as a single long line */
  fprintf(gp, "plot ");
  if (aPlotRes) {
    fprintf(gp, "0 t \"\", "); /* plot line to represent solution */
  }
  unsigned int ncol, npoints = 0;
  for (ncol = col1; ncol <= col2; ncol++) {
    /* plot polynomial and values for the specified CCD column */
    int red = (ncol - col1) / dcol, /* red */
        grn = (col2 - ncol) / dcol, /* green */
        blu = 0;                    /* blue */
    if (aPlotRes) {
      fprintf(gp, "\"-\" u 2:3 t \"col %u\" w p ps 0.8 lt rgb \"#%02x%02x%02x\"",
              ncol, red, grn, blu);
    } else {
      /* create data values from polynomial and residuals for given CCD column */
      fprintf(gp, "p(%u, x) t \"\" w l lw 0.7 lt rgb \"#%02x%02x%02x\", "
              "\"-\" u 1:(p(%u,$1)+$3) t \"col %u\" w p ps 0.8 lt rgb \"#%02x%02x%02x\"",
              ncol, red, grn, blu, ncol, ncol, red, grn, blu);
    }
    if (ncol == col2) {
      fprintf(gp, "\n"); /* end the plot command */
    } else {
      fprintf(gp, ", "); /* plot command of next column to come */
    }
  } /* for ncol (relevant columns) */
  for (ncol = col1; ncol <= col2; ncol++) {
    for (n = 0; n < nrow; n++) {
      if (x[n] == (int)ncol) {
        fprintf(gp, "%f %f %g\n", y[n], lambda[n], r[n]);
        npoints++;
      }
    }
    fprintf(gp, "EOF\n");
  } /* for ncol (relevant columns) */
  printf("Plotted %u points.\n", npoints);
  fflush(gp);
  /* request keypress, so that working with the mouse keeps   *
   * working and gnuplot has enough time to actually draw all *
   * the stuff before the files get removed                   */
  printf("Press ENTER to end program and close plot\n");
  getchar();
  pclose(gp);
  return CPL_ERROR_NONE;
#else /* no HAVE_POPEN && HAVE_PCLOSE */
  return CPL_ERROR_UNSUPPORTED_MODE;
#endif /* HAVE_POPEN && HAVE_PCLOSE */
} /* muse_wave_plot_column() */

/**@}*/
