/* -*- 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-2018 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                                       *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <math.h>
#include <string.h>

#include "muse_flux.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_cplwrappers.h"
#include "muse_pfits.h"
#include "muse_quality.h"
#include "muse_resampling.h"
#include "muse_utils.h"
#include "muse_wcs.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_flux         Flux calibration
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @brief  Allocate memory for a new <tt>muse_flux_object</tt> object.
  @return a new <tt>muse_flux_object *</tt> or @c NULL on error
  @remark The returned object has to be deallocated using
          <tt>muse_flux_object_delete()</tt>.
  @remark This function does not allocate the contents of the elements, these
          have to be allocated with the respective @c *_new() functions.

  Allocate memory to store the pointers of the <tt>muse_flux_object</tt>
  structure.
  Set the <tt>raref</tt> and <tt>decref</tt> components to NAN to signify that
  they are unset.
 */
/*----------------------------------------------------------------------------*/
muse_flux_object *
muse_flux_object_new(void)
{
  muse_flux_object *flux = cpl_calloc(1, sizeof(muse_flux_object));
  /* signify non-filled reference coordinates */
  flux->raref = NAN;
  flux->decref = NAN;
  return flux;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Deallocate memory associated to a muse_flux_object.
  @param  aFluxObj   input MUSE flux object

  Just calls the required @c *_delete() functions for each component before
  freeing the memory for the pointer itself.
  As a safeguard, it checks if a valid pointer was passed, so that crashes
  cannot occur.
 */
/*----------------------------------------------------------------------------*/
void
muse_flux_object_delete(muse_flux_object *aFluxObj)
{
  if (!aFluxObj) {
    return;
  }
  muse_datacube_delete(aFluxObj->cube);
  aFluxObj->cube = NULL;
  muse_image_delete(aFluxObj->intimage);
  aFluxObj->intimage = NULL;
  cpl_table_delete(aFluxObj->reference);
  aFluxObj->reference = NULL;
  cpl_table_delete(aFluxObj->sensitivity);
  aFluxObj->sensitivity = NULL;
  muse_table_delete(aFluxObj->response);
  aFluxObj->response = NULL;
  muse_table_delete(aFluxObj->telluric);
  aFluxObj->telluric = NULL;
  cpl_table_delete(aFluxObj->tellbands);
  aFluxObj->tellbands = NULL;
  cpl_free(aFluxObj);
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Compute average sampling for a MUSE-format flux reference table
  @param  aTable   the STD_FLUX_TABLE
  @return The sampling in Angstrom per bin or 0 on error.

  @error{set CPL_ERROR_NULL_INPUT\, return 0., aTable is NULL}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_reference_table_sampling(cpl_table *aTable)
{
  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, 0.);
  cpl_table_unselect_all(aTable);
  cpl_table_or_selected_double(aTable, "lambda", CPL_NOT_LESS_THAN,
                               kMuseNominalLambdaMin);
  cpl_table_and_selected_double(aTable, "lambda", CPL_NOT_GREATER_THAN,
                                kMuseNominalLambdaMax);
  cpl_size nsel = cpl_table_count_selected(aTable);
  cpl_array *asel = cpl_table_where_selected(aTable);
  cpl_size *sel = cpl_array_get_data_cplsize(asel);
  double lmin = cpl_table_get_double(aTable, "lambda", sel[0], NULL),
         lmax = cpl_table_get_double(aTable, "lambda", sel[nsel - 1], NULL);
  cpl_array_delete(asel);
  return (lmax - lmin) / nsel;
} /* muse_flux_reference_table_sampling() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Check and/or adapt the standard flux reference table format.
  @param  aTable   the input STD_FLUX_TABLE
  @return CPL_ERROR_NONE on successful check or conversion, another
          cpl_error_code on failure.

  We need a table with columns "lambda" and "flux" (and, optionally, "fluxerr")
  for the standard response calculation. The table columns all need to be in
  double format, and in the right units ("Angstrom", "erg/s/cm**2/Angstrom").
  If the wrong units are used for "lambda" and/or "flux" the table is rejected
  completely as incompatible, if only "fluxerr" has an unrecognized unit, that
  column is erased.

  Alternatively, we can accept HST CALSPEC tables which basically have the same
  information, just with different column names ("WAVELENGTH", "FLUX", and
  "STATERROR" plus "SYSERROR"), using different strings the the same units
  ("ANGSTROMS", "FLAM").

  @note This function cannot check, if the input table is in vacuum or air
        wavelengths. But if the input is in HST CALSPEC format, then it assumes
        that the wavelength given in the "WAVELENGTH" column are in vacuum. It
        then converts them to air wavelengths using the standard IAU conversion
        formula.

  @error{return CPL_ERROR_NULL_INPUT, aTable is NULL}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         a table with unrecognized format was found}
  @error{propagate CPL error, a (table column casting) operation did not work}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_reference_table_check(cpl_table *aTable)
{
  cpl_ensure_code(aTable, CPL_ERROR_NULL_INPUT);

  const char *flam1 = "erg/s/cm**2/Angstrom",
             *flam2 = "erg/s/cm^2/Angstrom";

  cpl_error_code rc = CPL_ERROR_NONE;
  cpl_errorstate prestate = cpl_errorstate_get();
  /* check two different types of tables: MUSE specific or HST CALSPEC */
  if (cpl_table_has_column(aTable, "lambda") &&
      cpl_table_has_column(aTable, "flux") &&
      cpl_table_get_column_unit(aTable, "lambda") &&
      cpl_table_get_column_unit(aTable, "flux") &&
      !strncmp(cpl_table_get_column_unit(aTable, "lambda"), "Angstrom", 9) &&
      (!strncmp(cpl_table_get_column_unit(aTable, "flux"), flam1, strlen(flam1)) ||
       !strncmp(cpl_table_get_column_unit(aTable, "flux"), flam2, strlen(flam2)))) {
    /* normal case: MUSE STD_FLUX_TABLE as specified; still need to        *
     * check, if we need to convert the column types (could be e.g. float) */
    if (cpl_table_get_column_type(aTable, "lambda") != CPL_TYPE_DOUBLE) {
      cpl_msg_debug(__func__, "Casting lambda column to double");
      cpl_table_cast_column(aTable, "lambda", NULL, CPL_TYPE_DOUBLE);
    }
    if (cpl_table_get_column_type(aTable, "flux") != CPL_TYPE_DOUBLE) {
      cpl_msg_debug(__func__, "Casting flux column to double");
      cpl_table_cast_column(aTable, "flux", NULL, CPL_TYPE_DOUBLE);
    }
    /* check optional column */
    if (cpl_table_has_column(aTable, "fluxerr")) {
      if (cpl_table_get_column_type(aTable, "fluxerr") != CPL_TYPE_DOUBLE) {
        cpl_msg_debug(__func__, "Casting fluxerr column to double");
        cpl_table_cast_column(aTable, "fluxerr", NULL, CPL_TYPE_DOUBLE);
      }
      const char *unit = cpl_table_get_column_unit(aTable, "fluxerr");
      if (!unit || (strncmp(unit, flam1, strlen(flam1)) &&
                    strncmp(unit, flam2, strlen(flam2)))) {
        cpl_msg_debug(__func__, "Erasing fluxerr column because of unexpected "
                      "unit (%s)", unit);
        cpl_table_erase_column(aTable, "fluxerr"); /* wrong unit, erase */
      }
    } /* if has fluxerr */
    cpl_msg_info(__func__, "Found MUSE format, average sampling %.3f Angstrom/bin"
                 " over MUSE range", muse_flux_reference_table_sampling(aTable));
  } else if (cpl_table_has_column(aTable, "WAVELENGTH") &&
      cpl_table_has_column(aTable, "FLUX") &&
      cpl_table_get_column_unit(aTable, "WAVELENGTH") &&
      cpl_table_get_column_unit(aTable, "FLUX") &&
      !strncmp(cpl_table_get_column_unit(aTable, "WAVELENGTH"), "ANGSTROMS", 10) &&
      !strncmp(cpl_table_get_column_unit(aTable, "FLUX"), "FLAM", 5)) {
#if 0
    printf("input HST CALSPEC table:\n");
    cpl_table_dump_structure(aTable, stdout);
    cpl_table_dump(aTable, cpl_table_get_nrow(aTable)/2, 3, stdout);
    fflush(stdout);
#endif
    /* other allowed case: HST CALSPEC format */
    cpl_table_cast_column(aTable, "WAVELENGTH", "lambda", CPL_TYPE_DOUBLE);
    cpl_table_cast_column(aTable, "FLUX", "flux", CPL_TYPE_DOUBLE);
    cpl_table_erase_column(aTable, "WAVELENGTH");
    cpl_table_erase_column(aTable, "FLUX");
    cpl_table_set_column_unit(aTable, "lambda", "Angstrom");
    cpl_table_set_column_unit(aTable, "flux", flam1);
    /* if the table comes with the typical STATERROR/SYSERROR separation, *
     * convert them into a single combined fluxerr column                 */
    if (cpl_table_has_column(aTable, "STATERROR") &&
        cpl_table_has_column(aTable, "SYSERROR") &&
        cpl_table_get_column_unit(aTable, "STATERROR") &&
        cpl_table_get_column_unit(aTable, "SYSERROR") &&
        !strncmp(cpl_table_get_column_unit(aTable, "STATERROR"), "FLAM", 5) &&
        !strncmp(cpl_table_get_column_unit(aTable, "SYSERROR"), "FLAM", 5)) {
      /* Cast to double before, not to lose precision, then compute *
       *   fluxerr = sqrt(STATERROR**2 + SYSERROR**2)               */
      cpl_table_cast_column(aTable, "STATERROR", "fluxerr", CPL_TYPE_DOUBLE);
      cpl_table_erase_column(aTable, "STATERROR");
      cpl_table_cast_column(aTable, "SYSERROR", NULL, CPL_TYPE_DOUBLE);
      cpl_table_power_column(aTable, "fluxerr", 2);
      cpl_table_power_column(aTable, "SYSERROR", 2);
      cpl_table_add_columns(aTable, "fluxerr", "SYSERROR");
      cpl_table_erase_column(aTable, "SYSERROR");
      cpl_table_power_column(aTable, "fluxerr", 0.5);
      cpl_table_set_column_unit(aTable, "fluxerr", flam1);
    } /* if error columns */
    /* XXX how to handle invalid entries in the STATERROR column *
     *     in telluric regions (e.g. in gd105_005.fits)?         */
    /* XXX how to handle DATAQUAL column? */

    /* erase further columns we don't need */
    if (cpl_table_has_column(aTable, "FWHM")) {
      cpl_table_erase_column(aTable, "FWHM");
    }
    if (cpl_table_has_column(aTable, "DATAQUAL")) {
      cpl_table_erase_column(aTable, "DATAQUAL");
    }
    if (cpl_table_has_column(aTable, "TOTEXP")) {
      cpl_table_erase_column(aTable, "TOTEXP");
    }
    /* convert from vacuum to air wavelengths */
    cpl_size irow, nrow = cpl_table_get_nrow(aTable);
    for (irow = 0; irow < nrow; irow++) {
      double lambda = cpl_table_get_double(aTable, "lambda", irow, NULL);
      cpl_table_set_double(aTable, "lambda", irow,
                           muse_astro_wavelength_vacuum_to_air(lambda));
    } /* for irow (all table rows) */
#if 0
    printf("converted HST CALSPEC table:\n");
    cpl_table_dump_structure(aTable, stdout);
    cpl_table_dump(aTable, cpl_table_get_nrow(aTable)/2, 3, stdout);
    fflush(stdout);
#endif
    cpl_msg_info(__func__, "Found HST CALSPEC format on input, converted to "
                 "MUSE format; average sampling %.3f Angstrom/bin over MUSE "
                 "range (assumed vacuum wavelengths on input, converted to air).",
                 muse_flux_reference_table_sampling(aTable));
  } else {
    cpl_msg_error(__func__, "Unknown format found!");
#if 0
    cpl_table_dump_structure(aTable, stdout);
#endif
    rc = CPL_ERROR_INCOMPATIBLE_INPUT;
  } /* else: no recognized format */

  /* check for errors in the above (casting!) before returning */
  if (!cpl_errorstate_is_equal(prestate)) {
    rc = cpl_error_get_code();
  }
  return rc;
} /* muse_flux_reference_table_check() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Compute linearly interpolated response of some kind at given
          wavelength.
  @param  aResponse   the response table
  @param  aLambda     wavelength to query
  @param  aError      the error connected to the interpolated datapoint, can
                      be NULL
  @param  aType       interpolation type
  @return The interpolated response; on error return 0. (1. for aType ==
          MUSE_FLUX_TELLURIC).

  This function uses binary search to linearly interpolate a response curve at
  the correct interval. The response table can be a filter (aType ==
  MUSE_FLUX_RESP_FILTER), a flux response curve (MUSE_FLUX_RESP_FLUX), a
  standard star spectrum (MUSE_FLUX_RESP_STD_FLUX), an atmospheric extinction
  curve (MUSE_FLUX_RESP_EXTINCT), or a telluric correction (MUSE_FLUX_TELLURIC).

  The table aResponse has to contain at least the column "lambda" (listing the
  wavelength in Angstroms).
  The other necessary columns depend on aType:
  - "throughput" (for filter curve, with the relative response for each
    wavelength),
  - "response" and "resperr" (for flux response curve, the response factor and
    its error, respectively),
  - "flux" and "fluxerr" (for standard star spectrum, the flux and its error,
    respectively),
  - "extinction" (for the extinction curve).
  - "ftelluric" and "ftellerr" (for the telluric correction factor and its
    error, respectively),
  All columns are expected to be of type CPL_TYPE_DOUBLE.

  @error{set CPL_ERROR_NULL_INPUT\, return 0. or 1., the input filter is NULL}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return 0. or 1., the type is unknown}
  @error{propagate CPL error\, return 0. or 1., the table has less than 1 rows}
  @error{propagate CPL error\, return 0. or 1.,
         the table data could not be returned}
 */
/*----------------------------------------------------------------------------*/
double
muse_flux_response_interpolate(const cpl_table *aResponse, double aLambda,
                               double *aError, muse_flux_interpolation_type aType)
{
  double rv = 0.;
  if (aType == MUSE_FLUX_TELLURIC) {
    rv = 1.;
  }
  cpl_ensure(aResponse, CPL_ERROR_NULL_INPUT, rv);
  int size = cpl_table_get_nrow(aResponse);
  cpl_ensure(size > 0, cpl_error_get_code(), rv);

  /* access the correct table column(s) depending on the type */
  const double *lbda = cpl_table_get_data_double_const(aResponse, "lambda"),
               *resp = NULL, *rerr = NULL;
  switch (aType) {
  case MUSE_FLUX_RESP_FILTER:
    resp = cpl_table_get_data_double_const(aResponse, "throughput");
    break;
  case MUSE_FLUX_RESP_FLUX:
    resp = cpl_table_get_data_double_const(aResponse, "response");
    if (aError) {
      rerr = cpl_table_get_data_double_const(aResponse, "resperr");
    }
    break;
  case MUSE_FLUX_RESP_STD_FLUX:
    resp = cpl_table_get_data_double_const(aResponse, "flux");
    if (aError) {
      rerr = cpl_table_get_data_double_const(aResponse, "fluxerr");
    }
    break;
  case MUSE_FLUX_RESP_EXTINCT:
    resp = cpl_table_get_data_double_const(aResponse, "extinction");
    break;
  case MUSE_FLUX_TELLURIC:
    resp = cpl_table_get_data_double_const(aResponse, "ftelluric");
    if (aError) {
      rerr = cpl_table_get_data_double_const(aResponse, "ftellerr");
    }
    break;
  default:
    cpl_error_set(__func__, CPL_ERROR_UNSUPPORTED_MODE);
    return rv;
  } /* switch aType */
  cpl_ensure(lbda && resp, cpl_error_get_code(), rv);
  if (aError) {
    cpl_ensure(rerr, cpl_error_get_code(), rv);
  }

  /* outside wavelength range of table */
  if (aLambda < lbda[0]) {
    return rv;
  }
  if (aLambda > lbda[size-1]) {
    return rv;
  }

  /* binary search for the correct wavelength */
  double response = rv, resperror = 0.;
  int l = 0, r = size - 1, /* left and right end */
      m = (l + r) / 2; /* middle index */
  while (CPL_TRUE) {
    if (aLambda >= lbda[m] && aLambda <= lbda[m+1]) {
      /* found right interval, so interpolate */
      double lquot = (aLambda - lbda[m]) / (lbda[m+1] - lbda[m]);
      response = resp[m] + (resp[m+1] - resp[m]) * lquot;
      if (rerr) { /* missing error information should be non-fatal */
        /* checked again that the derivatives leading to this error estimate *
         * are correct; apparently it's normal then, that the resulting      *
         * errors can be smaller than the two separate input errors          */
        resperror = sqrt(pow(rerr[m] * (1 - lquot), 2.)
                         + pow(rerr[m+1] * lquot, 2.));
      }
#if 0
      cpl_msg_debug(__func__, "Found at m=%d (%f: %f+/-%f) / "
                    "m+1=%d (%f: %f+/-%f) -> %f: %f+/-%f",
                    m, lbda[m], resp[m], rerr ? rerr[m] : 0.,
                    m+1, lbda[m+1], resp[m+1], rerr ? rerr[m+1] : 0.,
                    aLambda, response, resperror);
#endif
      break;
    }
    /* create next interval */
    if (aLambda < lbda[m]) {
      r = m;
    }
    if (aLambda > lbda[m]) {
      l = m;
    }
    m = (l + r) / 2;
  } /* while */

#if 0
  cpl_msg_debug(__func__, "Response %g+/-%g at lambda=%fA", response, resperror,
                aLambda);
#endif
  if (aError && rerr) {
    *aError = resperror;
  }
  return response;
} /* muse_flux_response_interpolate() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Background measurement using four medians around a central window.
  @param  aImage      the image to use
  @param  aX          horizontal center
  @param  aY          vertical center
  @param  aHalfSize   halfsize of the window
  @param  aDSky       width of the rectangular sky region around the window
  @param  aError      error estimate of the returned flux (can be NULL)
  @return the (median) background value or 0 on error

  @error{return 0\, set aError to FLT_MAX\, propagate CPL error code,
         median determination in all four sky regions fails}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_image_sky(cpl_image *aImage, double aX, double aY, double aHalfSize,
                    unsigned int aDSky, float *aError)
{
  if (aError) {
    *aError = FLT_MAX;
  }
  /* coordinates of inner window */
  int x1 = aX - aHalfSize, x2 = aX + aHalfSize,
      y1 = aY - aHalfSize, y2 = aY + aHalfSize;
  unsigned char nskyarea = 0;
  double skylevel = 0., skyerror = 0.;
  /* left */
  cpl_errorstate state = cpl_errorstate_get();
  cpl_stats_mode mode = CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV;
  cpl_stats *s = cpl_stats_new_from_image_window(aImage, mode,
                                                 x1 - aDSky, y1, x1 - 1, y2);
  if (s) {
    /* only if there was no error, the area was inside the image      *
     * boundaries and we can use it, otherwise the value is undefined */
    nskyarea++;
    skylevel += cpl_stats_get_median(s);
    skyerror += pow(cpl_stats_get_median_dev(s), 2);
    cpl_stats_delete(s);
  }
  /* right */
  s = cpl_stats_new_from_image_window(aImage,mode,
                                      x2 + 1, y1, x2 + aDSky, y2);
  if (s) {
    nskyarea++;
    skylevel += cpl_stats_get_median(s);
    skyerror += pow(cpl_stats_get_median_dev(s), 2);
    cpl_stats_delete(s);
  }
  /* bottom */
  s = cpl_stats_new_from_image_window(aImage,mode,
                                      x1, y1 - aDSky, x2, y1 - 1);
  if (s) {
    nskyarea++;
    skylevel += cpl_stats_get_median(s);
    skyerror += pow(cpl_stats_get_median_dev(s), 2);
    cpl_stats_delete(s);
  }
  /* top */
  s = cpl_stats_new_from_image_window(aImage,mode,
                                      x1, y2 + 1, x2, y2 + aDSky);
  if (s) {
    nskyarea++;
    skylevel += cpl_stats_get_median(s);
    skyerror += pow(cpl_stats_get_median_dev(s), 2);
    cpl_stats_delete(s);
  }
  if (nskyarea == 0) {
    return 0.;
  }
  skylevel /= nskyarea;
  skyerror = sqrt(skyerror) / nskyarea;
  if (!cpl_errorstate_is_equal(state)) { /* reset error code */
    cpl_errorstate_set(state);
  }
#if 0
  cpl_msg_debug(__func__, "skylevel = %f +/- %f (%u sky areas)",
                skylevel, skyerror, nskyarea);
#endif
  if (aError) {
    *aError = skyerror;
  }
  return skylevel;
} /* muse_flux_image_sky() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Flux integration using a Gaussian fit around a position in the image.
  @param  aImage      the image to use
  @param  aImErr      the error image to use (in sigmas not variances)
  @param  aX          horizontal center (first guess estimate)
  @param  aY          vertical center (first guess estimate)
  @param  aHalfSize   half size of the window to use for the Gaussian fit
  @param  aDSky       width of the rectangular sky region around the window
  @param  aMaxBad     maximum number of bad pixels to accept (ignored here!)
  @param  aFErr       pointer to the value where to save the flux error
  @return the integrated flux

  @error{return 0\, set CPL_ERROR_ILLEGAL_INPUT\, set aFErr to high value,
         Gaussian fit fails}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_image_gaussian(cpl_image *aImage, cpl_image *aImErr, double aX,
                         double aY, double aHalfSize, unsigned int aDSky,
                         unsigned int aMaxBad, float *aFErr)
{
  /* there is no simple way to count bad pixels inside an *
   * image window, so ignore this argument at the moment  */
  UNUSED_ARGUMENT(aMaxBad);

  if (aFErr) { /* set high variance for an error case */
    *aFErr = FLT_MAX;
  }

  cpl_array *params = cpl_array_new(7, CPL_TYPE_DOUBLE),
            *parerr = cpl_array_new(7, CPL_TYPE_DOUBLE);
  /* Set some first-guess parameters to help the fitting function.      *
   * Just set background and central position, cpl_fit_image_gaussian() *
   * finds good defaults for everything else.                           */
  cpl_errorstate state = cpl_errorstate_get();
  double skylevel = muse_flux_image_sky(aImage, aX, aY, aHalfSize, aDSky, NULL);
  if (!cpl_errorstate_is_equal(state)) {
    /* if background determination fails, a default of 0 should *
     * be good enough, so that we can ignore this failure       */
    cpl_errorstate_set(state);
  }
  cpl_array_set_double(params, 0, skylevel);
  cpl_array_set_double(params, 3, aX);
  cpl_array_set_double(params, 4, aY);
  double rms = 0, chisq = 0;
  /* function wants full widths but at most out to the image boundary */
  int nx = cpl_image_get_size_x(aImage),
      ny = cpl_image_get_size_y(aImage),
      xsize = fmin(aHalfSize, fmin(aX - 1., nx - aX)) * 2,
      ysize = fmin(aHalfSize, fmin(aY - 1., ny - aY)) * 2;
  cpl_error_code rc = cpl_fit_image_gaussian(aImage, aImErr, aX, aY, xsize, ysize,
                                             params, parerr, NULL, &rms, &chisq,
                                             NULL, NULL, NULL, NULL, NULL);
  if (rc != CPL_ERROR_NONE) {
    if (rc != CPL_ERROR_ILLEGAL_INPUT) {
      cpl_msg_debug(__func__, "rc = %d: %s", rc, cpl_error_get_message());
    }
    cpl_array_delete(params);
    cpl_array_delete(parerr);
    return 0;
  }
  double flux = cpl_array_get_double(params, 1, NULL),
         ferr = cpl_array_get_double(parerr, 1, NULL);
#if 0 /* DEBUG */
  double fwhmx = cpl_array_get_double(params, 5, NULL) * CPL_MATH_FWHM_SIG,
         fwhmy = cpl_array_get_double(params, 6, NULL) * CPL_MATH_FWHM_SIG;
  cpl_msg_debug(__func__, "%.3f,%.3f: %g+/-%g (bg: %g, FWHM: %.3f,%.3f, %g, %g)",
                cpl_array_get_double(params, 3, NULL), cpl_array_get_double(params, 4, NULL),
                flux, ferr, cpl_array_get_double(params, 0, NULL), fwhmx, fwhmy,
                rms, chisq);
#endif
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "skylevel = %f", cpl_array_get_double(params, 0, NULL));
  cpl_msg_debug(__func__, "measured flux %f +/- %f", flux, ferr);
#endif
  cpl_array_delete(params);
  cpl_array_delete(parerr);
  if (aFErr) {
    *aFErr = ferr;
  }
  return flux;
} /* muse_flux_image_gaussian() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Flux integration using a Moffat fit around a position in the image.
  @param  aImage      the image to use
  @param  aImErr      the error image to use (in sigmas not variances)
  @param  aX          horizontal center (first guess estimate)
  @param  aY          vertical center (first guess estimate)
  @param  aHalfSize   half size of the window to use for the Moffat fit
  @param  aDSky       width of the rectangular sky area around the window
  @param  aMaxBad     maximum number of bad pixels to accept
  @param  aFErr       pointer to the value where to save the flux error
  @return the integrated flux

  @error{return 0\, set error to high value,
         less than 16 good pixels or more than aMaxBad bad pixels were found}
  @error{return 0\, set CPL_ERROR_ILLEGAL_INPUT\, set aFErr to high value,
         Moffat fit fails}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_image_moffat(cpl_image *aImage, cpl_image *aImErr, double aX,
                       double aY, double aHalfSize, unsigned int aDSky,
                       unsigned int aMaxBad, float *aFErr)
{
  if (aFErr) { /* set high variance for an error case */
    *aFErr = FLT_MAX;
  }
  /* extract image regions around the fiducial peak into matrix and vectors */
  int x1 = aX - aHalfSize, x2 = aX + aHalfSize,
      y1 = aY - aHalfSize, y2 = aY + aHalfSize,
      nx = cpl_image_get_size_x(aImage),
      ny = cpl_image_get_size_y(aImage);
  if (x1 < 1) {
    x1 = 1;
  }
  if (x2 > nx) {
    x2 = nx;
  }
  if (y1 < 1) {
    y1 = 1;
  }
  if (y2 > ny) {
    y2 = ny;
  }
  int npoints = (x2 - x1 + 1) * (y2 - y1 + 1);

  cpl_matrix *pos = cpl_matrix_new(npoints, 2);
  cpl_vector *values = cpl_vector_new(npoints),
             *errors = cpl_vector_new(npoints);
  float *derr = cpl_image_get_data_float(aImErr);
  int i, idx = 0;
  for (i = x1; i <= x2; i++) {
    int j;
    for (j = y1; j <= y2; j++) {
      int err;
      double data = cpl_image_get(aImage, i, j, &err);
      if (err) { /* bad pixel or error */
        continue;
      }
      cpl_matrix_set(pos, idx, 0, i);
      cpl_matrix_set(pos, idx, 1, j);
      cpl_vector_set(values, idx, data);
      cpl_vector_set(errors, idx, derr[(i-1) + (j-1)*nx]);
      idx++;
    } /* for j (y pixels) */
  } /* for i (x pixels) */
  /* need at least something like 4x4 pixels for a solid fit;    *
   * also check missing entries against max number of bad pixels */
  if (idx < 16 || (npoints - idx) > (int)aMaxBad) {
    cpl_matrix_delete(pos);
    cpl_vector_delete(values);
    cpl_vector_delete(errors);
    return 0;
  }
  cpl_matrix_set_size(pos, idx, 2);
  cpl_vector_set_size(values, idx);
  cpl_vector_set_size(errors, idx);

  cpl_array *params = cpl_array_new(8, CPL_TYPE_DOUBLE),
            *parerr = cpl_array_new(8, CPL_TYPE_DOUBLE);
  /* Set some first-guess parameters to help the fitting function. */
  cpl_errorstate state = cpl_errorstate_get();
  double skylevel = muse_flux_image_sky(aImage, aX, aY, aHalfSize, aDSky, NULL);
  if (!cpl_errorstate_is_equal(state)) {
    /* if background determination fails, a default of 0 should *
     * be good enough, so that we can ignore this failure       */
    cpl_errorstate_set(state);
  }
  cpl_array_set_double(params, 0, skylevel);
  cpl_array_set_double(params, 2, aX);
  cpl_array_set_double(params, 3, aY);
  double rms = 0, chisq = 0;
  cpl_error_code rc = muse_utils_fit_moffat_2d(pos, values, errors,
                                               params, parerr, NULL,
                                               &rms, &chisq);
  cpl_matrix_delete(pos);
  cpl_vector_delete(values);
  cpl_vector_delete(errors);
  if (rc != CPL_ERROR_NONE) {
    if (rc != CPL_ERROR_ILLEGAL_INPUT) {
      cpl_msg_debug(__func__, "rc = %d: %s", rc, cpl_error_get_message());
    }
    cpl_array_delete(params);
    cpl_array_delete(parerr);
    return 0;
  }

  double flux = cpl_array_get_double(params, 1, NULL);
  if (aFErr) {
    *aFErr = cpl_array_get_double(parerr, 1, NULL);
  }
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "skylevel = %f", cpl_array_get_double(params, 0, NULL));
  cpl_msg_debug(__func__, "measured flux %f +/- %f", flux, cpl_array_get_double(parerr, 1, NULL));
#endif
  cpl_array_delete(params);
  cpl_array_delete(parerr);
  return flux;
} /* muse_flux_image_moffat() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Simple flux integration in a square image window.
  @param  aImage      the image to use
  @param  aImErr      the error image to use (in sigmas not variances)
  @param  aX          horizontal center
  @param  aY          vertical center
  @param  aHalfSize   half size of the window to use for the flux integration
  @param  aDSky       width of the rectangular sky region around the window
  @param  aMaxBad     maximum number of bad pixels to accept
  @param  aFErr       pointer to the value where to save the flux error
  @return the integrated flux

  @error{return 0\, set CPL_ERROR_ILLEGAL_INPUT\, set aFErr to high value,
         more than aMaxBad bad pixels were found within measurement area}
  @error{return 0\, set CPL_ERROR_DATA_NOT_FOUND\, set aFErr to high value,
         background determination failed}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_image_square(cpl_image *aImage, cpl_image *aImErr, double aX,
                       double aY, double aHalfSize, unsigned int aDSky,
                       unsigned int aMaxBad, float *aFErr)
{
  if (aFErr) { /* set high variance for an error case */
    *aFErr = FLT_MAX;
  }
  int x1 = aX - aHalfSize, x2 = aX + aHalfSize,
      y1 = aY - aHalfSize, y2 = aY + aHalfSize,
      nx = cpl_image_get_size_x(aImage),
      ny = cpl_image_get_size_y(aImage);
  if (x1 < 1) {
    x1 = 1;
  }
  if (x2 > nx) {
    x2 = nx;
  }
  if (y1 < 1) {
    y1 = 1;
  }
  if (y2 > ny) {
    y2 = ny;
  }
  float skyerror;
  cpl_errorstate state = cpl_errorstate_get();
  double skylevel = muse_flux_image_sky(aImage, aX, aY, aHalfSize, aDSky,
                                        &skyerror);
  if (!cpl_errorstate_is_equal(state)) {
    /* background determination is critical for this method, *
     * reset the error but return with zero anyway           */
    cpl_errorstate_set(state);
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    return 0.; /* fail on missing background level */
  }

  /* extract the measurement region; fail on too many bad pixels, *
   * but interpolate, if there are only a few bad ones            */
  cpl_image *region = cpl_image_extract(aImage, x1, y1, x2, y2);
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "region [%d:%d,%d:%d] %"CPL_SIZE_FORMAT" bad pixels",
                x1, y1, x2, y2, cpl_image_count_rejected(region));
#endif
  if (cpl_image_count_rejected(region) > aMaxBad) {
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    cpl_image_delete(region);
    return 0.; /* fail on too many bad pixels */
  }
  cpl_image *regerr = cpl_image_extract(aImErr, x1, y1, x2, y2);
  if (cpl_image_count_rejected(region) > 0) {
    cpl_detector_interpolate_rejected(region);
    cpl_detector_interpolate_rejected(regerr);
  }

  /* integrated flux, subtracted by the sky over the size of the *
   * aperture; an error modeled approximately after IRAF phot    */
  int npoints = (x2 - x1 + 1) * (y2 - y1 + 1),
      /* number of sky pixels should be counted in muse_flux_image_sky(), *
       * but it's really not so important to add another parameter, the   *
       * error that we compute here is probably too small to be useful... */
      nsky = 2 * aDSky * (x2 - x1 + y2 - y1 + 2);
  double flux = cpl_image_get_flux(region) - skylevel * npoints,
         ferr = sqrt(cpl_image_get_sqflux(regerr)
                     + npoints * skyerror*skyerror * (1. + (double)npoints / nsky));
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "measured flux %f +/- %f (%d object pixels, %d pixels"
                " with %f with sky %f)", flux, ferr, npoints, nsky,
                cpl_image_get_flux(region), cpl_image_get_sqflux(regerr));
#endif
  cpl_image_delete(region);
  cpl_image_delete(regerr);
  if (aFErr) {
    *aFErr = ferr;
  }
  return flux;
} /* muse_flux_image_square() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Flux integration in a circle with an annular background.
  @param  aImage    the image to use
  @param  aImErr    the error image to use (in sigmas not variances)
  @param  aX        horizontal center
  @param  aY        vertical center
  @param  aAper     radius of the circle to use for the flux integration
  @param  aAnnu     inner radius of annulus to use for the background
  @param  aDAnnu    width of the background annulus
  @param  aMaxBad   maximum number of bad pixels to accept
  @param  aFErr     pointer to the value where to save the flux error
  @return the integrated flux

  @error{return 0\, set error to high value\, set CPL_ERROR_DATA_NOT_FOUND,
         no valid pixels found in the background annulus}
  @error{return 0\, set error to high value\, set CPL_ERROR_ILLEGAL_INPUT,
         more than aMaxBad bad pixels were found within measurement area}
 */
/*----------------------------------------------------------------------------*/
static double
muse_flux_image_circle(cpl_image *aImage, cpl_image *aImErr, double aX,
                        double aY, double aAper, double aAnnu, double aDAnnu,
                        unsigned int aMaxBad, float *aFErr)
{
  if (aFErr) { /* set high variance, for error cases */
    *aFErr = FLT_MAX;
  }
  double rmax = ceil(fmax(aAper, aAnnu + aDAnnu));
  int x1 = aX - rmax, x2 = aX + rmax,
      y1 = aY - rmax, y2 = aY + rmax,
      nx = cpl_image_get_size_x(aImage),
      ny = cpl_image_get_size_y(aImage);
  if (x1 < 1) {
    x1 = 1;
  }
  if (x2 > nx) {
    x2 = nx;
  }
  if (y1 < 1) {
    y1 = 1;
  }
  if (y2 > ny) {
    y2 = ny;
  }
  /* first loop to collect the background and *
   * count bad pixels inside the aperture     */
  cpl_vector *vbg = cpl_vector_new((x2 - x1 + 1) * (y2 - y1 + 1)),
             *vbe = cpl_vector_new((x2 - x1 + 1) * (y2 - y1 + 1));
  unsigned int nbad = 0, nbg = 0;
  int i;
  for (i = x1; i <= x2; i++) {
    int j;
    for (j = y1; j <= y2; j++) {
      double r = sqrt(pow(aX - i, 2) + pow(aY - j, 2));
      if (r <= aAper) {
        nbad += cpl_image_is_rejected(aImage, i, j) == 1;
      }
      if (r < aAnnu || r > aAnnu + aDAnnu) {
        continue;
      }
      int err;
      double value = cpl_image_get(aImage, i, j, &err);
      if (err) { /* exclude bad pixels */
        continue;
      }
      cpl_vector_set(vbg, nbg, value);
      cpl_vector_set(vbe, nbg, cpl_image_get(aImErr, i, j, &err));
      nbg++;
    } /* for j (vertical pixels) */
  } /* for i (horizontal pixels) */
  if (nbg <= 0) {
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    cpl_vector_delete(vbg);
    cpl_vector_delete(vbe);
    return 0.; /* fail on missing background pixels */
  }
  cpl_vector_set_size(vbg, nbg);
  cpl_vector_set_size(vbe, nbg);
  cpl_matrix *pos = cpl_matrix_new(1, nbg); /* we don't care about positions... */
  double mse;
  cpl_polynomial *fit = muse_utils_iterate_fit_polynomial(pos, vbg, vbe, NULL,
                                                          0, 3., &mse, NULL);
#if 0 /* DEBUG */
  unsigned int nrej = nbg - cpl_vector_get_size(vbg);
#endif
  nbg = cpl_vector_get_size(vbg);
  cpl_size pows = 0; /* get the zero-order coefficient */
  double smean = cpl_polynomial_get_coeff(fit, &pows),
         sstdev = sqrt(mse);
  cpl_polynomial_delete(fit);
  cpl_matrix_delete(pos);
  cpl_vector_delete(vbg);
  cpl_vector_delete(vbe);
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "sky: %d pixels (%d rejected), %f +/- %f; found %d "
                "bad pixels inside aperture", nbg, nrej, smean, sstdev, nbad);
#endif
  if (nbad > aMaxBad) { /* too many bad pixels inside integration area? */
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    return 0.; /* fail on too many bad pixels */
  }

  /* now replace the few bad pixels by interpolation */
  if (nbad > 0) {
    cpl_detector_interpolate_rejected(aImage);
    cpl_detector_interpolate_rejected(aImErr);
  }

  /* second loop to integrate the flux */
  double flux = 0.,
         ferr = 0.;
  unsigned int nobj = 0;
  for (i = x1; i <= x2; i++) {
    int j;
    for (j = y1; j <= y2; j++) {
      double r = sqrt(pow(aX - i, 2) + pow(aY - j, 2));
      if (r > aAper) {
        continue;
      }
      int err;
      double value = cpl_image_get(aImage, i, j, &err),
             error = cpl_image_get(aImErr, i, j, &err);
      flux += value;
      ferr += error*error;
      nobj++;
    } /* for j (vertical pixels) */
  } /* for i (horizontal pixels) */
  flux -= smean * nobj;
  /* Compute error like IRAF phot:                                       *
   *    error = sqrt (flux / epadu + area * stdev**2 +                   *
   *                  area**2 * stdev**2 / nsky)                         *
   * We take our summed error instead of the error computed via the gain */
  ferr = sqrt(ferr + nobj * sstdev*sstdev * (1. + (double)nobj / nbg));
#if 0 /* DEBUG */
  cpl_msg_debug(__func__, "flux: %d pixels (%d interpolated), %f +/- %f",
                nobj, nbad, flux, ferr);
#endif
  if (aFErr) {
    *aFErr = ferr;
  }
  return flux;
} /* muse_flux_image_circle() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Integrate the flux of the standard star(s) given a datacube.
  @param  aCube        input datacube with the standard star
  @param  aApertures   apertures of detected objects in the cube
  @param  aProfile     the spatial profile to use for flux integration
  @return a muse_image * with the integrated fluxes of all objects or NULL on
          error
  @remark aProfile can be one of MUSE_FLUX_PROFILE_GAUSSIAN,
          MUSE_FLUX_PROFILE_MOFFAT, MUSE_FLUX_PROFILE_CIRCLE, and
          MUSE_FLUX_PROFILE_EQUAL_SQUARE.

  Use the input datacube (aCube) and the detections in it (aApertures) to
  determine the FWHM of the objects, and use it to define the flux integration
  window (as three times the FWHM). Integrate the flux of each object for all
  wavelength bins, using either simple flux integration or profile fitting
  depending on aProfile.

  @note The area over which the flux integration is measured, depends on the
        seeing. For most methods the half-size is at least 3x the measured FWHM
        at each wavelength. Only for MUSE_FLUX_PROFILE_CIRCLE, it is forced to
        be 4x the FWHM, where the FWHM is the measured value on a central plane
        of the reconstructed cube, with a fall-back to the DIMM seeing from the
        FITS header.

  Store the flux measurements in a two-dimensional image, where each row
  corresponds to one detected object, and the three image components are data,
  data quality, and variance. It also carries a standard spectral WCS in its
  header component.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         inputs aCube or aApertures are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         the input profile type is unknown}
 */
/*----------------------------------------------------------------------------*/
muse_image *
muse_flux_integrate_cube(muse_datacube *aCube, cpl_apertures *aApertures,
                         muse_flux_profile_type aProfile)
{
  cpl_ensure(aCube && aApertures, CPL_ERROR_NULL_INPUT, NULL);
  switch (aProfile) {
  case MUSE_FLUX_PROFILE_GAUSSIAN:
    cpl_msg_info(__func__, "Gaussian profile fits for flux integration");
    break;
  case MUSE_FLUX_PROFILE_MOFFAT:
    cpl_msg_info(__func__, "Moffat profile fits for flux integration");
    break;
  case MUSE_FLUX_PROFILE_CIRCLE:
    cpl_msg_info(__func__, "Circular flux integration");
    break;
  case MUSE_FLUX_PROFILE_EQUAL_SQUARE:
    cpl_msg_info(__func__, "Simple square window flux integration");
    break;
  default:
    cpl_msg_error(__func__, "Unknown flux integration method!");
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    return NULL;
  }

  /* construct image of number of wavelengths x number of stars */
  int naper = cpl_apertures_get_size(aApertures), /* can only be > 0 */
      nlambda = cpl_imagelist_get_size(aCube->data),
      nplane = cpl_imagelist_get_size(aCube->data) / 2; /* central plane */
  cpl_image *cim = cpl_imagelist_get(aCube->data, nplane);
  muse_image *intimage = muse_image_new();
  intimage->data = cpl_image_new(nlambda, naper, CPL_TYPE_FLOAT);
  intimage->dq = cpl_image_new(nlambda, naper, CPL_TYPE_INT);
  intimage->stat = cpl_image_new(nlambda, naper, CPL_TYPE_FLOAT);
  /* copy wavelength WCS from 3rd axis of cube to x-axis of image */
  intimage->header = cpl_propertylist_new();
  cpl_propertylist_append_double(intimage->header, "CRVAL1",
                                 muse_pfits_get_crval(aCube->header, 3));
  cpl_propertylist_append_double(intimage->header, "CRPIX1",
                                 muse_pfits_get_crpix(aCube->header, 3));
  cpl_propertylist_append_double(intimage->header, "CD1_1",
                                 muse_pfits_get_cd(aCube->header, 3, 3));
  cpl_propertylist_append_string(intimage->header, "CTYPE1",
                                 muse_pfits_get_ctype(aCube->header, 3));
  cpl_propertylist_append_string(intimage->header, "CUNIT1",
                                 muse_pfits_get_cunit(aCube->header, 3));
  /* fill the 2nd axis with standards */
  cpl_propertylist_append_double(intimage->header, "CRVAL2", 1.);
  cpl_propertylist_append_double(intimage->header, "CRPIX2", 1.);
  cpl_propertylist_append_double(intimage->header, "CD2_2", 1.);
  cpl_propertylist_append_string(intimage->header, "CTYPE2", "PIXEL");
  cpl_propertylist_append_string(intimage->header, "CUNIT2", "pixel");
  cpl_propertylist_append_double(intimage->header, "CD1_2", 0.);
  cpl_propertylist_append_double(intimage->header, "CD2_1", 0.);
  /* we need the date, data units, exposure time, and instrument mode, too */
  cpl_propertylist_append_string(intimage->header, "DATE-OBS",
                                 cpl_propertylist_get_string(aCube->header,
                                                             "DATE-OBS"));
  cpl_propertylist_append_string(intimage->header, "BUNIT",
                                 muse_pfits_get_bunit(aCube->header));
  cpl_propertylist_append_double(intimage->header, "EXPTIME",
                                 muse_pfits_get_exptime(aCube->header));
  cpl_propertylist_append_string(intimage->header, "ESO INS MODE",
                                 cpl_propertylist_get_string(aCube->header,
                                                             "ESO INS MODE"));
  if (cpl_propertylist_has(aCube->header, MUSE_HDR_FLUX_FFCORR)) {
    cpl_propertylist_append_bool(intimage->header, MUSE_HDR_FLUX_FFCORR, CPL_TRUE);
    cpl_propertylist_set_comment(intimage->header, MUSE_HDR_FLUX_FFCORR,
                                 MUSE_HDR_FLUX_FFCORR_C);
  }

  /* get DIMM seeing from the headers, convert it to size in pixels */
  cpl_errorstate ps = cpl_errorstate_get();
  double dimm = (muse_pfits_get_fwhm_start(aCube->header)
                 + muse_pfits_get_fwhm_end(aCube->header)) / 2.;
  cpl_boolean goodDIMM = cpl_errorstate_is_equal(ps);
  if (!goodDIMM) {
    cpl_errorstate_set(ps);
  }
  if (muse_pfits_get_mode(aCube->header) < MUSE_MODE_NFM_AO_N) {
    dimm /= (kMuseSpaxelSizeX_WFM + kMuseSpaxelSizeY_WFM) / 2.;
  } else { /* for NFM */
    dimm /= (kMuseSpaxelSizeX_NFM + kMuseSpaxelSizeY_NFM) / 2.;
  }
  /* estimate the "real" PSF FWHM */
  double fwhm = -1., /* set bad value to begin with */
         xc = cpl_apertures_get_centroid_x(aApertures, 1),
         yc = cpl_apertures_get_centroid_y(aApertures, 1),
         xfwhm, yfwhm;
  cpl_image_get_fwhm(cim, lround(xc), lround(yc), &xfwhm, &yfwhm);
  if (xfwhm > 0. && yfwhm > 0.) {
    fwhm = (xfwhm + yfwhm) / 2.;
  } else if (xfwhm > 0.) {
    fwhm = xfwhm;
  } else if (yfwhm > 0.) {
    fwhm = yfwhm;
  }
  ps = cpl_errorstate_get();
  cpl_boolean AO = muse_pfits_get_ho_loop(aCube->header),
              IRLOS = muse_pfits_get_ir_loop(aCube->header),
              nfmAO = AO && IRLOS               /* NFM with AO correction on? */
                    && muse_pfits_get_mode(aCube->header) == MUSE_MODE_NFM_AO_N;
  if (!cpl_errorstate_is_equal(ps)) {
    cpl_errorstate_set(ps);
  }
  if (fwhm > 0. && (!nfmAO || (nfmAO && aProfile <= MUSE_FLUX_PROFILE_GAUSSIAN))) {
    /* use empirical FWHM only if it works on this wavelength plane *
     * and if we are not using NFM with AO correction active        */
    cpl_msg_info(__func__, "Using empirically estimated reference FWHM (%.3f "
                 "pix) instead of DIMM seeing (%.3f pix), AO is %sactive.",
                 fwhm, dimm, AO ? "" : "in");
  } else if (goodDIMM) {
    if (AO && !nfmAO) {
      cpl_msg_warning(__func__, "Using DIMM seeing (%.3f pix) for reference "
                      "FWHM, but AO is active!", dimm);
    } else {
      cpl_msg_info(__func__, "Using DIMM seeing (%.3f pix) for reference FWHM.",
                   dimm);
    }
    fwhm = dimm;
  } else {
    cpl_msg_warning(__func__, "No good initial FWHM estimate, falling back to 5"
                    " pix estimate as last resort!");
    fwhm = 5.; /* assume a well-sampled but compact PSF */
  }

  /* track the sizes used for the fits/integrations in an image, *
   * record the half-sizes (or radiuses) in on row per object    */
  cpl_image *sizes = cpl_image_new(nlambda, naper, CPL_TYPE_DOUBLE);
  double *psizes = cpl_image_get_data_double(sizes);
  /* access pointers for the flux-image */
  float *data = cpl_image_get_data_float(intimage->data),
        *stat = cpl_image_get_data_float(intimage->stat);
  int *dq = cpl_image_get_data_int(intimage->dq);
  /* loop over all wavelengths and measure the flux */
  int l, ngood = 0, nillegal = 0, nbadbg = 0; /* count good fits and errors */
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aApertures, aCube, aProfile, data, dq, fwhm, naper, nbadbg,   \
                 ngood, nillegal, nlambda, psizes, stat)                       \
          private(xc, yc, xfwhm, yfwhm)
  for (l = 0; l < nlambda; l++) {
    cpl_image *plane = cpl_imagelist_get(aCube->data, l),
              *pldq = aCube->dq ? cpl_imagelist_get(aCube->dq, l) : NULL,
              *plerr = cpl_image_duplicate(cpl_imagelist_get(aCube->stat, l));
#if 0 /* DEBUG */
    cpl_stats *stats = cpl_stats_new_from_image(plerr, CPL_STATS_ALL);
    cpl_msg_debug(__func__, "lambda = %d/%f %s", l + 1,
                  (l + 1 - muse_pfits_get_crpix(aCube->header, 3))
                  * muse_pfits_get_cd(aCube->header, 3, 3)
                  + muse_pfits_get_crval(aCube->header, 3),
                  muse_pfits_get_cunit(aCube->header, 3));
    cpl_msg_debug(__func__, "variance: %g...%g...%g", cpl_stats_get_min(stats),
                  cpl_stats_get_mean(stats), cpl_stats_get_max(stats));
    cpl_stats_delete(stats);
#endif
    /* make sure to exclude bad pixels from the fits below */
    if (pldq) {
      muse_quality_image_reject_using_dq(plane, pldq, plerr);
    } else {
      cpl_image_reject_value(plane, CPL_VALUE_NAN); /* mark NANs */
      cpl_image_reject_value(plerr, CPL_VALUE_NAN);
    }
#if 0 /* DEBUG */
    stats = cpl_stats_new_from_image(plerr, CPL_STATS_ALL);
    cpl_msg_debug(__func__, "cut variance: %g...%g...%g (%"CPL_SIZE_FORMAT" bad"
                  " pixel)", cpl_stats_get_min(stats), cpl_stats_get_mean(stats),
                  cpl_stats_get_max(stats), cpl_image_count_rejected(plane));
    cpl_stats_delete(stats);
#endif
    /* convert variable to sigmas */
    cpl_image_power(plerr, 0.5);
#if 0 /* DEBUG */
    stats = cpl_stats_new_from_image(plerr, CPL_STATS_ALL);
    cpl_msg_debug(__func__, "errors: %g...%g...%g", cpl_stats_get_min(stats),
                  cpl_stats_get_mean(stats), cpl_stats_get_max(stats));
    cpl_stats_delete(stats);
#endif
    cpl_errorstate state = cpl_errorstate_get();
    int n;
    for (n = 1; n <= naper; n++) {
      /* use detection aperture to construct much larger one for measurement */
      xc = cpl_apertures_get_centroid_x(aApertures, n);
      yc = cpl_apertures_get_centroid_y(aApertures, n);
      double size = sqrt(cpl_apertures_get_npix(aApertures, n));
      cpl_errorstate prestate = cpl_errorstate_get();
      cpl_image_get_fwhm(plane, lround(xc), lround(yc), &xfwhm, &yfwhm);
      if (xfwhm < 0 || yfwhm < 0) {
        data[l + (n-1) * nlambda] = 0.;
        stat[l + (n-1) * nlambda] = FLT_MAX;
        cpl_errorstate_set(prestate);
        continue;
      }
      /* half size for flux integration, at least 3 x FWHM */
      double halfsize = fmax(1.5 * (xfwhm + yfwhm), 3. * fwhm);
      if (halfsize < size / 2) { /* at least the size of the det. aperture */
        halfsize = size / 2;
      }
      /* compute the maximum distance that we can cover within the field */
      int nx = cpl_image_get_size_x(plane),
          ny = cpl_image_get_size_y(plane);
      double dx1 = xc - 1,
             dx2 = nx - xc,
             dy1 = yc - 1,
             dy2 = ny - yc,
             /* min distance to edge, with 5 pixel buffer */
             dmin = fmin(fmin(dx1, dx2), fmin(dy1, dy2)) - 5.;
      cpl_boolean rsize = CPL_FALSE; /* if size was restricted by the FOV */
      if (halfsize > dmin) {
#if 0 /* DEBUG */
        cpl_msg_debug(__func__, "%.1f %.1f %.1f %.1f -> %.1f", dx1, dx2,
                      dy1, dy2, dmin);
#endif
        halfsize = dmin;
        rsize = CPL_TRUE; /* in this case, do restrict the size */
      }
      psizes[l + (n-1) * nlambda] = halfsize;
#if 0 /* DEBUG */
      cpl_msg_debug(__func__, "%.2f,%.2f FWHM %.2f %.2f size %.2f --> %.2f",
                    xc, yc, xfwhm, yfwhm, size, halfsize * 2.);
#endif

      switch (aProfile) {
      case MUSE_FLUX_PROFILE_GAUSSIAN:
        data[l + (n-1) * nlambda] = muse_flux_image_gaussian(plane, plerr, xc, yc,
                                                             halfsize, 5, 10,
                                                             &stat[l + (n-1) * nlambda]);
        break;
      case MUSE_FLUX_PROFILE_MOFFAT:
        data[l + (n-1) * nlambda] = muse_flux_image_moffat(plane, plerr, xc, yc,
                                                           halfsize, 5, 10,
                                                           &stat[l + (n-1) * nlambda]);
        break;
      case MUSE_FLUX_PROFILE_CIRCLE: {
        /* The circular method needs larger region to properly integrate *
         * everything at least something like 4 x FWHM to be sure; for   *
         * very large apertures (see dmin/rsize above) this won't work.  *
         * Normally, start with some buffer between background annulus   *
         * and integration aperture, but for very large apertures        *
         * there is no space, so just one extra pixel of buffer.         */
        double radius = rsize ? halfsize : 4./3. * halfsize,
               rannu = rsize ? radius + 1. : radius * 5. / 4.;
        /* allow more rejected pixels when the aperture is very large   *
         * (it might include part of the border that contains no data!) */
        int maxrej = rsize ? 100 : 10;
        psizes[l + (n-1) * nlambda] = radius;
        data[l + (n-1) * nlambda] = muse_flux_image_circle(plane, plerr, xc, yc,
                                                           radius, rannu, 10, maxrej,
                                                           &stat[l + (n-1) * nlambda]);
        break;
      } /* case MUSE_FLUX_PROFILE_CIRCLE */
      default: { /* MUSE_FLUX_PROFILE_EQUAL_SQUARE */
        /* If we applied the size restriction, make the box even smaller, *
         * so that the FOV can also fit the background region around it.  */
        double hs = rsize ? halfsize - 10. : halfsize;
        data[l + (n-1) * nlambda] = muse_flux_image_square(plane, plerr, xc, yc,
                                                           hs, 5, 10,
                                                           &stat[l + (n-1) * nlambda]);
      }
      } /* switch */
      if (data[l + (n-1) * nlambda] < 0 || !isfinite(data[l + (n-1) * nlambda])) {
        data[l + (n-1) * nlambda] = 0.; /* should not contribute to flux */
        dq[l + (n-1) * nlambda] = EURO3D_MISSDATA; /* mark as bad in DQ extension */
        stat[l + (n-1) * nlambda] = FLT_MAX;
      }
    } /* for n (all apertures) */

    /* count "Illegal input" errors and for those reset the state, as there   *
     * can be many of them (one for each incompletely filled wavelength plane */
    if (!cpl_errorstate_is_equal(state)) {
      if (cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT) {
        cpl_errorstate_set(state);
        #pragma omp atomic
        nillegal++;
      } else if (cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND) {
        cpl_errorstate_set(state);
        #pragma omp atomic
        nbadbg++;
      }
    } else {
      #pragma omp atomic
      ngood++;
    }

    cpl_image_delete(plerr);
  } /* for l (all wavelengths) */

  /* output statistics for the sizes used, mask out unset positions in the image */
  cpl_image_reject_value(sizes, CPL_VALUE_ZERO);
  int n;
  for (n = 1; n <= naper; n++) {
    if (aProfile == MUSE_FLUX_PROFILE_CIRCLE) {
      cpl_msg_info(__func__, "Radiuses used for circular flux integration for "
                   "object %d: %f +/- %f (%f) %f..%f", n,
                   cpl_image_get_mean_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_stdev_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_median_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_min_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_max_window(sizes, 1, n, nlambda, n));
    } else {
      cpl_msg_info(__func__, "Half-sizes used for flux integration for object "
                   "%d: %f +/- %f (%f) %f..%f", n,
                   cpl_image_get_mean_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_stdev_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_median_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_min_window(sizes, 1, n, nlambda, n),
                   cpl_image_get_max_window(sizes, 1, n, nlambda, n));
    } /* else */
  } /* for n (all apertures) */
#if 0
  cpl_image_save(sizes, "sizes.fits", CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
#endif
  cpl_image_delete(sizes);

  /* add headers about the objects to the integrated image */
  cpl_propertylist_append_int(intimage->header, MUSE_HDR_FLUX_NOBJ, naper);
  cpl_propertylist_set_comment(intimage->header, MUSE_HDR_FLUX_NOBJ,
                               MUSE_HDR_FLUX_NOBJ_C);
  /* create a basic WCS, assuming nominal MUSE properties, for an *
   * estimate of the celestial position of all detected objects   */
  cpl_propertylist *wcs1 = muse_wcs_create_default(NULL),
                   *wcs = muse_wcs_apply_cd(aCube->header, wcs1);
  cpl_propertylist_delete(wcs1);
  /* update WCS just as in muse_resampling_cube() */
  double crpix1 = muse_pfits_get_crpix(aCube->header, 1)
                + (1. + cpl_image_get_size_x(cim)) / 2.,
         crpix2 = muse_pfits_get_crpix(aCube->header, 2)
                + (1. + cpl_image_get_size_y(cim)) / 2.;
  cpl_propertylist_update_double(wcs, "CRPIX1", crpix1);
  cpl_propertylist_update_double(wcs, "CRPIX2", crpix2);
  cpl_propertylist_update_double(wcs, "CRVAL1", muse_pfits_get_ra(aCube->header));
  cpl_propertylist_update_double(wcs, "CRVAL2", muse_pfits_get_dec(aCube->header));
  for (n = 1; n <= naper; n++) {
    /* use detection aperture to construct much larger one for measurement */
    xc = cpl_apertures_get_centroid_x(aApertures, n);
    yc = cpl_apertures_get_centroid_y(aApertures, n);
    double ra, dec;
    muse_wcs_celestial_from_pixel(wcs, xc, yc, &ra, &dec);
    double flux = cpl_image_get_flux_window(intimage->data, 1, n, nlambda, n);
    cpl_msg_debug(__func__, "Object %02d: %.3f,%.3f pix, %f,%f deg, flux %e %s",
                  n, xc, yc, ra, dec, flux, muse_pfits_get_bunit(intimage->header));
    char kw[KEYWORD_LENGTH];
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_X, n);
    cpl_propertylist_append_float(intimage->header, kw, xc);
    cpl_propertylist_set_comment(intimage->header, kw, MUSE_HDR_FLUX_OBJn_X_C);
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_Y, n);
    cpl_propertylist_append_float(intimage->header, kw, yc);
    cpl_propertylist_set_comment(intimage->header, kw, MUSE_HDR_FLUX_OBJn_Y_C);
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_RA, n);
    cpl_propertylist_append_double(intimage->header, kw, ra);
    cpl_propertylist_set_comment(intimage->header, kw, MUSE_HDR_FLUX_OBJn_RA_C);
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_DEC, n);
    cpl_propertylist_append_double(intimage->header, kw, dec);
    cpl_propertylist_set_comment(intimage->header, kw, MUSE_HDR_FLUX_OBJn_DEC_C);
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_FLUX, n);
    cpl_propertylist_append_double(intimage->header, kw, flux);
    cpl_propertylist_set_comment(intimage->header, kw, MUSE_HDR_FLUX_OBJn_FLUX_C);
  } /* for n (all apertures) */
  cpl_propertylist_delete(wcs);

  if (nillegal > 0 || nbadbg > 0) {
    cpl_msg_warning(__func__, "Successful fits in %d wavelength planes, but "
                    "encountered %d \"Illegal input\" errors and %d bad "
                    "background determinations", ngood, nillegal, nbadbg);
  } else {
    cpl_msg_info(__func__, "Successful fits in %d wavelength planes", ngood);
  }

  return intimage;
} /* muse_flux_integrate_cube() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Reconstruct a cube, detect the standard star, and integrate its flux.
  @param  aPixtable   the input pixel table of the standard star exposure
  @param  aProfile    the spatial profile to use for flux integration
  @param  aFluxObj    the MUSE flux object to modify with the cube and
                      integrated flux
  @return CPL_ERROR_NONE on success, another CPL error code on failure
  @remark The flux image returned in aFluxObj contains the flux measurements,
          with the fluxes in the data extension of the muse_image. The stat
          extension contains the measurement errors for each wavelength and the
          header element is used to propagate the WCS keywords to define the
          wavelength scale. Each image row contains the fluxes of one standard
          star, so that the vertical image size is equal to the measured stars
          on success.
  @remark aProfile can be one of MUSE_FLUX_PROFILE_GAUSSIAN,
          MUSE_FLUX_PROFILE_MOFFAT, MUSE_FLUX_PROFILE_CIRCLE, and
          MUSE_FLUX_PROFILE_EQUAL_SQUARE.

  Resample the input pixel table to a cube, with wavelength sampling matched to
  the MUSE spectral sampling. Find objects (lowering the S/N between 50 and 5,
  in multiple steps) on the central plane of the cube. Create apertures for all
  detections and integrate their flux in each wavelength.
  See @ref muse_flux_integrate_cube() for details on the flux integration.

  @note If the input pixel table contains data of an AO instrument mode, the
        notch filter flag (EURO3D_NOTCH_NAD) is reset before resampling into
        the cube, so that useful throughput values can be computed.

  Both the resampled datacube and the flux measurements image are added into the
  aFluxObj structure (components intimage and cube).

  @error{return CPL_ERROR_NULL_INPUT, inputs aPixtable or aFluxObj are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, the input profile type is unknown}
  @error{return CPL_ERROR_DATA_NOT_FOUND, no objects found}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_integrate_std(muse_pixtable *aPixtable, muse_flux_profile_type aProfile,
                        muse_flux_object *aFluxObj)
{
  cpl_ensure_code(aPixtable && aFluxObj, CPL_ERROR_NULL_INPUT);
  switch (aProfile) {
  case MUSE_FLUX_PROFILE_GAUSSIAN:
  case MUSE_FLUX_PROFILE_MOFFAT:
  case MUSE_FLUX_PROFILE_CIRCLE:
  case MUSE_FLUX_PROFILE_EQUAL_SQUARE:
    break;
  default:
    return cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
  }

  /* if remove notch filter flags from NaD range, since we do want a   *
   * (near-zero) throughput measurement inside that filtered range; *
   * but keep original DQ column around to reset it later           */
  muse_ins_mode insmode = muse_pfits_get_mode(aPixtable->header);
  if (insmode >= MUSE_MODE_WFM_AO_E) {
    cpl_table_duplicate_column(aPixtable->table, MUSE_PIXTABLE_DQ"_NAD",
                               aPixtable->table, MUSE_PIXTABLE_DQ);
    int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);
    cpl_size irow, nrow = muse_pixtable_get_nrow(aPixtable);
    for (irow = 0; irow < nrow; irow++) {
      if (dq[irow] & EURO3D_NOTCH_NAD) {
        dq[irow] &= ~EURO3D_NOTCH_NAD; /* reset that bit */
      } /* if */
    } /* for */
  } /* if AO mode */

  if (getenv("MUSE_DEBUG_FLUX") && atoi(getenv("MUSE_DEBUG_FLUX")) > 2) {
    const char *fn = "flux__pixtable.fits";
    cpl_msg_info(__func__, "Saving pixel table as \"%s\"", fn);
    muse_pixtable_save(aPixtable, fn);
  }
  muse_resampling_params *params =
    muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
  params->pfx = 1.; /* large pixfrac to be sure to cover most gaps */
  params->pfy = 1.;
  params->pfl = 1.;
  /* resample at nominal resolution, the conversion to [1/Angstrom] *
   * is later done when computing the sensitivity function          */
  params->dlambda = kMuseSpectralSamplingA;
  params->crtype = MUSE_RESAMPLING_CRSTATS_MEDIAN;
  params->crsigma = 25.;
  muse_datacube *cube = muse_resampling_cube(aPixtable, params, NULL);
  if (cube) {
    aFluxObj->cube = cube;
  }
  muse_resampling_params_delete(params);
  /* reinstate the original DQ column */
  if (cpl_table_has_column(aPixtable->table, MUSE_PIXTABLE_DQ"_NAD")) {
    cpl_table_erase_column(aPixtable->table, MUSE_PIXTABLE_DQ);
    cpl_table_name_column(aPixtable->table, MUSE_PIXTABLE_DQ"_NAD",
                          MUSE_PIXTABLE_DQ);
  } /* if */
  if (getenv("MUSE_DEBUG_FLUX") && atoi(getenv("MUSE_DEBUG_FLUX")) >= 2) {
    const char *fn = "flux__cube.fits";
    cpl_msg_info(__func__, "Saving cube as \"%s\"", fn);
    muse_datacube_save(aFluxObj->cube, fn);
  }
  int nplane = cpl_imagelist_get_size(cube->data) / 2; /* central plane */
  cpl_image *cim = cpl_imagelist_get(cube->data, nplane),
            *cdq = cpl_imagelist_get(cube->dq, nplane);
  /* make sure to reject bad pixels before using *
   * object detection using image statistics     */
  muse_quality_image_reject_using_dq(cim, cdq, NULL);
  /* use high sigmas for detection */
  double dsigmas[] = { 50., 30., 10., 8., 6., 5. };
  cpl_vector *vsigmas = cpl_vector_wrap(sizeof(dsigmas) / sizeof(double),
                                        dsigmas);
  cpl_size isigma = -1;
  cpl_apertures *apertures = cpl_apertures_extract(cim, vsigmas, &isigma);
  int napertures = apertures ? cpl_apertures_get_size(apertures) : 0;
  if (napertures < 1) {
    /* isigma is still -1 in this case, so take the last vector entry */
    cpl_msg_error(__func__, "No object for flux integration found down to %.1f"
                  " sigma limit on plane %d",
                  cpl_vector_get(vsigmas, cpl_vector_get_size(vsigmas) - 1),
                  nplane + 1);
    cpl_vector_unwrap(vsigmas);
    cpl_apertures_delete(apertures);
    return cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
  }
  cpl_msg_debug(__func__, "The %.1f sigma threshold was used to find %d object%s"
                " on plane %d", cpl_vector_get(vsigmas, isigma), napertures,
                napertures == 1 ? "" : "s", nplane + 1);
  cpl_vector_unwrap(vsigmas);
#if 0 /* DEBUG */
  cpl_apertures_dump(apertures, stdout);
  fflush(stdout);
#endif

  /* now do the flux integration */
  muse_image *intimage = muse_flux_integrate_cube(cube, apertures, aProfile);
  cpl_apertures_delete(apertures);
  aFluxObj->intimage = intimage; /* save integrated fluxes into in/out struct */

  return CPL_ERROR_NONE;
} /* muse_flux_integrate_std() */

/* prominent telluric absorption bands, together with clean regions: *
 *    [0]   lower limit of telluric region                           *
 *    [1]   upper limit of telluric region                           *
 *    [2]   lower limit of fit region (excludes telluric region)     *
 *    [3]   upper limit of fit region (excludes telluric region)     *
 * created from by-hand measurements on spectra from the ESO sky     *
 * model, and from the Keck list of telluric lines                   */
static const double kTelluricBands[][4] = {
  { 6273., 6320., 6213., 6380. }, /* now +/-60 Angstrom around... */
  { 6864., 6967., 6750., 7130. }, /* B-band */
  { 7164., 7325., 7070., 7580. },
  { 7590., 7700., 7470., 7830. }, /* A-band */
  { 8131., 8345., 7900., 8600. },
  { 8952., 9028., 8850., 9082. }, /* very rough! */
  { 9274., 9770., 9080., 9263. }, /* very very rough! */
  {   -1.,   -1.,   -1.,   -1. }
};

/*----------------------------------------------------------------------------*/
/**
 * @brief Table definition for a telluric bands table.
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_response_tellbands_def[] = {
  { "lmin", CPL_TYPE_DOUBLE, "Angstrom", "%8.3f",
    "lower limit of the telluric region", CPL_TRUE },
  { "lmax", CPL_TYPE_DOUBLE, "Angstrom", "%8.3f",
    "upper limit of the telluric region", CPL_TRUE },
  { "bgmin", CPL_TYPE_DOUBLE, "Angstrom", "%8.3f",
    "lower limit of the background region", CPL_TRUE },
  { "bgmax", CPL_TYPE_DOUBLE, "Angstrom", "%8.3f",
    "upper limit of the background region", CPL_TRUE },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Set or create table of telluric band regions in the flux object.
  @param  aFluxObj     flux object for which to set the telluric bands
  @param  aTellBands   the table to set (optional, can be NULL)
  @return CPL_ERROR_NONE or another CPL error code on failure.

  @error{set and return CPL_ERROR_NULL_INPUT, aFluxObj is NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_flux_response_set_telluric_bands(muse_flux_object *aFluxObj,
                                      const cpl_table *aTellBands)
{
  cpl_ensure_code(aFluxObj, CPL_ERROR_NULL_INPUT);

  /* if a valid table was passed, just set that */
  if (aTellBands && muse_cpltable_check(aTellBands, muse_response_tellbands_def)
                    == CPL_ERROR_NONE) {
    cpl_msg_debug(__func__, "using given table for telluric bands");
    aFluxObj->tellbands = cpl_table_duplicate(aTellBands);
    return CPL_ERROR_NONE;
  }
  /* create a table for the default regions */
  unsigned int ntell = sizeof(kTelluricBands) / sizeof(kTelluricBands[0]) - 1;
  cpl_msg_debug(__func__, "using builtin regions for telluric bands (%u "
                "entries)", ntell);
  aFluxObj->tellbands = muse_cpltable_new(muse_response_tellbands_def, ntell);
  cpl_table *tb = aFluxObj->tellbands; /* shortcut */
  unsigned int k;
  for (k = 0; k < ntell; k++) {
    cpl_table_set_double(tb, "lmin", k, kTelluricBands[k][0]);
    cpl_table_set_double(tb, "lmax", k, kTelluricBands[k][1]);
    cpl_table_set_double(tb, "bgmin", k, kTelluricBands[k][2]);
    cpl_table_set_double(tb, "bgmax", k, kTelluricBands[k][3]);
  } /* for k */
  if (getenv("MUSE_DEBUG_FLUX") && atoi(getenv("MUSE_DEBUG_FLUX")) >= 2) {
    const char *fn = "flux__tellregions.fits";
    cpl_msg_info(__func__, "Saving telluric bands table as \"%s\"", fn);
    cpl_table_save(tb, NULL, NULL, fn, CPL_IO_CREATE);
  }
  return CPL_ERROR_NONE;
} /* muse_flux_response_set_telluric_bands() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Dump sensitivity table of a flux object into a file.
  @param  aFluxObj   flux object whose sensitivity component is dumped to a file
  @param  aName      the index of the star as measured (starting at 0)

  The filename for the output is created as "flux__sens_%s.ascii", where the
  "%s" string is aName.

  This function only does something if the environment variable MUSE_DEBUG_FLUX
  is set and positive.
 */
/*----------------------------------------------------------------------------*/
static void
muse_flux_response_dump_sensitivity(muse_flux_object *aFluxObj,
                                    const char *aName)
{
  char *dodebug = getenv("MUSE_DEBUG_FLUX");
  if (!dodebug || (dodebug && atoi(dodebug) <= 0)) {
    return;
  }
  char *fn = cpl_sprintf("flux__sens_%s.ascii", aName);
  FILE *fp = fopen(fn, "w");
  fprintf(fp, "#"); /* prefix first line (table header) for easier plotting */
  cpl_table_dump(aFluxObj->sensitivity, 0,
                 cpl_table_get_nrow(aFluxObj->sensitivity), fp);
  fclose(fp);
  cpl_msg_debug(__func__, "Written %"CPL_SIZE_FORMAT" datapoints to \"%s\"",
                cpl_table_get_nrow(aFluxObj->sensitivity), fn);
  cpl_free(fn);
} /* muse_flux_response_dump_sensitivity() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Convert measured and reference fluxes into a sensitivity table.
  @param  aFluxObj     flux object containing measurements (component intfluxes)
  @param  aStar        the index of the star as measured (starting at 0)
  @param  aReference   table containing the reference response for the star
  @param  aAirmass     the airmass of the measured exposure
  @param  aExtinct     the extinction table
  @return CPL_ERROR_NONE or another CPL error code on failure.

  Compare the measured flux at each wavelength to the reference flux using
  extinction curve and airmass to correct the result for the atmosphere.

  The result gets added as a new table (with the columns "lambda", "sens",
  "serr", and "dq") as the sensitivity component of the input aFluxObj
  structure.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aFluxObj or its intimage component are NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_flux_response_sensitivity(muse_flux_object *aFluxObj,
                               unsigned int aStar, const cpl_table *aReference,
                               double aAirmass, const cpl_table *aExtinct)
{
  cpl_ensure_code(aFluxObj && aFluxObj->intimage, CPL_ERROR_NULL_INPUT);

  double crval = muse_pfits_get_crval(aFluxObj->intimage->header, 1),
         cdelt = muse_pfits_get_cd(aFluxObj->intimage->header, 1, 1),
         crpix = muse_pfits_get_crpix(aFluxObj->intimage->header, 1),
         exptime = muse_pfits_get_exptime(aFluxObj->intimage->header);
  int nlambda = cpl_image_get_size_x(aFluxObj->intimage->data);

  aFluxObj->sensitivity = cpl_table_new(nlambda);
  cpl_table *sensitivity = aFluxObj->sensitivity;
  cpl_table_new_column(sensitivity, "lambda", CPL_TYPE_DOUBLE);
  cpl_table_new_column(sensitivity, "sens", CPL_TYPE_DOUBLE);
  cpl_table_new_column(sensitivity, "serr", CPL_TYPE_DOUBLE);
  cpl_table_new_column(sensitivity, "dq", CPL_TYPE_INT);
  cpl_table_set_column_format(sensitivity, "dq", "%u");
  float *data = cpl_image_get_data_float(aFluxObj->intimage->data),
        *stat = cpl_image_get_data_float(aFluxObj->intimage->stat);
  int l, idx = 0;
  for (l = 0; l < nlambda; l++) {
    if (data[l + aStar*nlambda] <= 0. ||
        stat[l + aStar*nlambda] <= 0. ||
        stat[l + aStar*nlambda] == FLT_MAX) { /* exclude bad fits */
      continue;
    }

    double lambda = crval + cdelt * (l + 1 - crpix),
           /* interpolate extinction curve at this wavelength */
           extinct = !aExtinct ? 0. /* no extinction term */
                   : muse_flux_response_interpolate(aExtinct, lambda, NULL,
                                                    MUSE_FLUX_RESP_EXTINCT);
    cpl_errorstate prestate = cpl_errorstate_get();
    double referr = 0.,
           ref = muse_flux_response_interpolate(aReference, lambda, &referr,
                                                MUSE_FLUX_RESP_STD_FLUX);
    /* on error, try again without trying to find the fluxerr column */
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_errorstate_set(prestate); /* don't want to propagate this error outside */
      ref = muse_flux_response_interpolate(aReference, lambda, NULL,
                                           MUSE_FLUX_RESP_STD_FLUX);
    }
    /* calibration factor at this wavelength: ratio of observed count *
     * rate corrected for extinction to expected flux in magnitudes   */
    double c = 2.5 * log10(data[l + aStar*nlambda]
                           / exptime / cdelt / ref)
             + aAirmass * extinct,
           cerr = sqrt(pow(referr / ref, 2) + stat[l + aStar*nlambda]
                                            / pow(data[l + aStar*nlambda], 2))
                * 2.5 / CPL_MATH_LN10;
    cpl_table_set_double(sensitivity, "lambda", idx, lambda);
    cpl_table_set_double(sensitivity, "sens", idx, c);
    cpl_table_set_double(sensitivity, "serr", idx, cerr);
    cpl_table_set_int(sensitivity, "dq", idx, EURO3D_GOODPIXEL);
    idx++;
  } /* for l (all wavelengths) */
  /* cut the data to the used size */
  cpl_table_set_size(sensitivity, idx);
  return CPL_ERROR_NONE;
} /* muse_flux_response_sensitivity() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Mark questionable sensitivity entries depending on wavelength.
  @param  aFluxObj       flux object containing sensitivity table
  @return CPL_ERROR_NONE or another CPL error code on failure.

  This function uses the "dq" column of the aFluxObj->sensitivity table to mark
  measurements in a region affected by telluric absorption with EURO3D_TELLURIC
  and those bluer than kMuseNominalCutoff or kMuseAOCutoff with
  EURO3D_OUTSDRANGE, if the instrument mode is not the extended wavelength
  range.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aFluxObj or its intimage component are NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_flux_response_mark_questionable(muse_flux_object *aFluxObj)
{
  cpl_ensure_code(aFluxObj && aFluxObj->intimage, CPL_ERROR_NULL_INPUT);

  cpl_propertylist *header = aFluxObj->intimage->header; /* shortcut */
  cpl_table *tsens = aFluxObj->sensitivity,
            *tb = aFluxObj->tellbands;

  /* check for MUSE setup: is it nominal wavelength range? */
  cpl_boolean isnominal = muse_pfits_get_mode(header) == MUSE_MODE_WFM_NONAO_N,
              isAON = (muse_pfits_get_mode(header) == MUSE_MODE_WFM_AO_N)
                    || (muse_pfits_get_mode(header) == MUSE_MODE_NFM_AO_N);
  /* exclude regions within the telluric absorption bands *
   * and outside wavelength range                         */
  int irow, nfluxes = cpl_table_get_nrow(tsens);
  for (irow = 0; irow < nfluxes; irow++) {
    double lambda = cpl_table_get_double(tsens, "lambda", irow, NULL);
    unsigned int dq = EURO3D_GOODPIXEL,
                 k, nk = cpl_table_get_nrow(tb);
    for (k = 0; k < nk; k++) {
      double lmin = cpl_table_get_double(tb, "lmin", k, NULL),
             lmax = cpl_table_get_double(tb, "lmax", k, NULL);
      if (lambda >= lmin && lambda <= lmax) {
        dq |= EURO3D_TELLURIC;
      }
    } /* for k */
    if ((isnominal && lambda < kMuseNominalCutoff) ||
        (isAON && lambda < kMuseAOCutoff)) {
      dq |= EURO3D_OUTSDRANGE;
    }
    cpl_table_set_int(tsens, "dq", irow, dq);
  } /* for irow (all table) */
  return CPL_ERROR_NONE;
} /* muse_flux_response_mark_questionable() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Fit a polynomial to a wavelength range in the sensitivity table.
  @param  aFluxObj   flux object containing sensitivity table
  @param  aLambda1   starting wavelength
  @param  aLambda2   end wavelength
  @param  aOrder     the polynomial order to use
  @param  aRSigma    the rejection sigma for the iterative fit
  @param  aRMSE      pointer to the reduced MSE value to return
  @return The polynomial fit

  This transforms the "lambda", "sens", and "serr" columns of the
  aFluxObj->sensitivity table into a matrix and two vectors and calls
  muse_utils_iterate_fit_polynomial() to do the actual iterative fit. Of the
  input table, only elements inside the wavelength range are used, and of those
  all entries with dq != EURO3D_GOODPIXEL | EURO3D_TELLCOR are ignored. If the
  iterative fit deletes entries, they are also removed from the
  aFluxObj->sensitivity table, "bad" entries and those outside the wavelength
  range are not affected.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aFluxObj or its sensitivity component are NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_polynomial *
muse_flux_response_fit(muse_flux_object *aFluxObj,
                       double aLambda1, double aLambda2,
                       unsigned int aOrder, double aRSigma, double *aRMSE)
{
  cpl_ensure(aFluxObj && aFluxObj->sensitivity, CPL_ERROR_NULL_INPUT, NULL);

  cpl_table *tsens = aFluxObj->sensitivity;
  cpl_table_select_all(tsens); /* default, but make sure it's true...*/
  cpl_table_and_selected_int(tsens, "dq", CPL_NOT_EQUAL_TO, EURO3D_GOODPIXEL);
  cpl_table_and_selected_int(tsens, "dq", CPL_NOT_EQUAL_TO, EURO3D_TELLCOR);
  cpl_table_or_selected_double(tsens, "lambda", CPL_LESS_THAN, aLambda1);
  cpl_table_or_selected_double(tsens, "lambda", CPL_GREATER_THAN, aLambda2);
  /* keep the "bad" ones around */
  cpl_table *tunwanted = cpl_table_extract_selected(tsens);
  cpl_table_erase_selected(tsens);
  muse_flux_response_dump_sensitivity(aFluxObj, "fitinput");

  /* convert sensitivity table to matrix (lambda) and vectors (sens *
   * and serr), exclude pixels marked as not EURO3D_GOODPIXEL       */
  int nrow = cpl_table_get_nrow(tsens);
  cpl_matrix *lambdas = cpl_matrix_new(1, nrow);
  cpl_vector *sens = cpl_vector_new(nrow),
             *serr = cpl_vector_new(nrow);
  memcpy(cpl_matrix_get_data(lambdas),
         cpl_table_get_data_double_const(tsens, "lambda"), nrow*sizeof(double));
  memcpy(cpl_vector_get_data(sens),
         cpl_table_get_data_double_const(tsens, "sens"), nrow*sizeof(double));
  memcpy(cpl_vector_get_data(serr),
         cpl_table_get_data_double_const(tsens, "serr"), nrow*sizeof(double));

  /* do the fit */
  double chisq, mse;
  cpl_polynomial *fit = muse_utils_iterate_fit_polynomial(lambdas, sens, serr,
                                                          tsens, aOrder, aRSigma,
                                                          &mse, &chisq);
  int nout = cpl_vector_get_size(sens);
#if 0
  cpl_msg_debug(__func__, "transferred %d entries (%.3f...%.3f) for the "
                "order %u fit, %d entries are left, RMS %f", nrow, aLambda1,
                aLambda2, aOrder, nout, sqrt(mse));
#endif
  cpl_matrix_delete(lambdas);
  cpl_vector_delete(sens);
  cpl_vector_delete(serr);
  if (aRMSE) {
    *aRMSE =  mse / (nout - aOrder - 1);
  }

  /* put "bad" entries back, at the end of the table */
  cpl_table_insert(tsens, tunwanted, nout);
  cpl_table_delete(tunwanted);
  return fit;
} /* muse_flux_response_fit() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Compute telluric correction factors.
  @param  aFluxObj   flux object containing sensitivity table
  @param  aAirmass   the airmass of the exposure
  @return CPL_ERROR_NONE or another CPL error code on failure.

  This adds two more columns "sens_orig" and "tellcor" to the
  aFluxObj->sensitivity table. It uses the wavelengths from the
  aFluxObj->tellbands table to fit 2nd order polynomials across telluric
  regions, stores the original values in "sens_orig" and replaces the entries in
  "sens" with the fitted values. Both values are then used to compute telluric
  correction factors, which are stored in the "tellcor" column. Finally, the
  airmass is applied following the approach from the Keck/HIRES data reduction
  (see "Makee: Atmospheric Absorption Correction",
  http://www2.keck.hawaii.edu/inst/hires/makeewww/Atmosphere/index.html).

  @error{set and return CPL_ERROR_NULL_INPUT, aFluxObj is NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_flux_response_telluric(muse_flux_object *aFluxObj, double aAirmass)
{
  cpl_ensure_code(aFluxObj, CPL_ERROR_NULL_INPUT);

  cpl_table *tsens = aFluxObj->sensitivity,
            *tb = aFluxObj->tellbands;
  cpl_table_new_column(tsens, "sens_orig", CPL_TYPE_DOUBLE);
  cpl_table_new_column(tsens, "serr_orig", CPL_TYPE_DOUBLE);
  cpl_table_new_column(tsens, "tellcor", CPL_TYPE_DOUBLE);
  unsigned int k, nk = cpl_table_get_nrow(tb);
  for (k = 0; k < nk; k++) {
    double lmin = cpl_table_get_double(tb, "lmin", k, NULL),
           lmax = cpl_table_get_double(tb, "lmax", k, NULL),
           bgmin = cpl_table_get_double(tb, "bgmin", k, NULL),
           bgmax = cpl_table_get_double(tb, "bgmax", k, NULL),
           datamin = cpl_table_get_column_min(tsens, "lambda"),
           datamax = cpl_table_get_column_max(tsens, "lambda");
    cpl_boolean extrapolate = CPL_FALSE;
    if (bgmax < lmax || datamax < lmax) {
      extrapolate = CPL_TRUE;
    }
    if (bgmin > lmin || datamin > lmin) {
      extrapolate = CPL_TRUE;
    }
    if (datamin > lmax || datamax < lmin) {
      /* no sense trying to even extrapolate linearly */
      cpl_msg_warning(__func__, "Telluric region %u (range %.2f...%.2f, "
                      "reference region %.2f...%.2f) outside data range "
                      "(%.2f..%.2f)!", k + 1, lmin, lmax, bgmin, bgmax,
                      datamin, datamax);
      continue;
    }
    /* if we extrapolate (often redward) then use a linear fit only */
    unsigned int order = extrapolate ? 1 : 2;
    /* the telluric regions themselves are already marked, so   *
     * they don't need to be touched again there before the fit */
    double rmse = 0.;
    cpl_errorstate state = cpl_errorstate_get();
    cpl_polynomial *fit = muse_flux_response_fit(aFluxObj, bgmin, bgmax,
                                                 order, 3., &rmse);
    if (!cpl_errorstate_is_equal(state) || !fit) {
      cpl_msg_warning(__func__, "Telluric region %u (range %.2f...%.2f, "
                      "reference region %.2f...%.2f) could not be fitted!",
                      k + 1, lmin, lmax, bgmin, bgmax);
      cpl_errorstate_set(state); /* swallow the errors */
      cpl_polynomial_delete(fit); /* just in case the polynomial was created */
      continue;
    }
    cpl_msg_debug(__func__, "Telluric region %u: %.2f...%.2f, reference region "
                  "%.2f...%.2f", k + 1, lmin, lmax, bgmin, bgmax);
#if 0
    cpl_polynomial_dump(fit, stdout);
    fflush(stdout);
#endif
    int irow, nrow = cpl_table_get_nrow(tsens);
    for (irow = 0; irow < nrow; irow++) {
      double lambda = cpl_table_get_double(tsens, "lambda", irow, NULL);
      if (lambda >= lmin && lambda <= lmax &&
          (unsigned int)cpl_table_get_int(tsens, "dq", irow, NULL)
          == EURO3D_TELLURIC) {
        double origval = cpl_table_get_double(tsens, "sens", irow, NULL),
               origerr = cpl_table_get_double(tsens, "serr", irow, NULL),
               interpval = cpl_polynomial_eval_1d(fit, lambda, NULL);
        cpl_table_set_int(tsens, "dq", irow, EURO3D_TELLCOR);
        cpl_table_set_double(tsens, "sens_orig", irow, origval);
        cpl_table_set_double(tsens, "sens", irow, interpval);
        /* correct the error bars of the fitted points *
         * by adding in quadrature the reduced MSE     */
        cpl_table_set_double(tsens, "serr_orig", irow, origerr);
        cpl_table_set_double(tsens, "serr", irow, sqrt(origerr*origerr + rmse));

        if (interpval > origval) {
          /* compute the factor, as flux ratio */
          double ftelluric = pow(10, -0.4 * (interpval - origval));
          cpl_table_set(tsens, "tellcor", irow, ftelluric);
        } else {
          cpl_table_set_double(tsens, "tellcor", irow, 1.);
        }
      }
    } /* for irow */
    cpl_polynomial_delete(fit);
  } /* for k (telluric line regions) */
  /* correct for the airmass */
  cpl_table_power_column(tsens, "tellcor", 1. / aAirmass);
  return CPL_ERROR_NONE;
} /* muse_flux_response_telluric() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief  Extrapolate the sensitivity function to cover the full MUSE range.
  @param  aFluxObj    flux object containing sensitivity table
  @param  aDistance   distance in Angstrom to use for extrapolation
  @return CPL_ERROR_NONE or another CPL error code on failure.

  This function adds artificial (but reasonable!) data beyond the measured
  sensitivity curve, to make flux calibration of MUSE data possible to the
  furthest wavelengths possibly recorded. The first and last aDistance Angstrom
  of the measured sensitivity are linearly extrapolated, resulting entries
  marked with dq = EURO3D_OUTSDRANGE and with errors (in the "serr" column) that
  start at the error of the extreme value and double every 50 Angstrom.
  The output aFluxObj->sensitivity table is sorted by increasing wavelength.

  @error{set and return CPL_ERROR_NULL_INPUT, aFluxObj is NULL}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_flux_response_extrapolate(muse_flux_object *aFluxObj, double aDistance)
{
  cpl_ensure_code(aFluxObj, CPL_ERROR_NULL_INPUT);

  cpl_table *tsens = aFluxObj->sensitivity;
  cpl_propertylist *order = cpl_propertylist_new();
  cpl_propertylist_append_bool(order, "lambda", CPL_FALSE);
  cpl_table_sort(tsens, order);

  int nrow = cpl_table_get_nrow(tsens);
  /* first and last good wavelength and corresponsing error */
  double lambda1 = cpl_table_get_double(tsens, "lambda", 0, NULL),
         serr1 = cpl_table_get_double(tsens, "serr", 0, NULL);
  unsigned int dq = cpl_table_get_int(tsens, "dq", 0, NULL);
  int irow = 0;
  while (dq != EURO3D_GOODPIXEL && dq != EURO3D_TELLCOR) {
    lambda1 = cpl_table_get_double(tsens, "lambda", ++irow, NULL);
    serr1 = cpl_table_get_double(tsens, "serr", irow, NULL);
    dq = cpl_table_get_int(tsens, "dq", irow, NULL);
  }
  double lambda2 = cpl_table_get_double(tsens, "lambda", nrow - 1, NULL),
         serr2 = cpl_table_get_double(tsens, "serr", nrow - 1, NULL);
  dq = cpl_table_get_int(tsens, "dq", nrow - 1, NULL);
  irow = nrow - 1;
  while (dq != EURO3D_GOODPIXEL && dq != EURO3D_TELLCOR) {
    lambda2 = cpl_table_get_double(tsens, "lambda", --irow, NULL);
    serr2 = cpl_table_get_double(tsens, "serr", irow, NULL);
    dq = cpl_table_get_int(tsens, "dq", irow, NULL);
  }
  cpl_polynomial *fit1 = muse_flux_response_fit(aFluxObj, lambda1,
                                                lambda1 + aDistance, 1, 5., NULL),
                 *fit2 = muse_flux_response_fit(aFluxObj, lambda2 - aDistance,
                                                lambda2, 1, 5., NULL);
  nrow = cpl_table_get_nrow(tsens); /* fitting may have erased rows! */
  double d1 = (lambda1 - kMuseLambdaMinX) / 100., /* want 10 additional entries... */
         d2 = (kMuseLambdaMaxX - lambda2) / 100.; /* ... at each end               */
  cpl_table_set_size(tsens, nrow + 200);
  irow = nrow;
  double l;
  if (fit1) { /* extrapolate blue end */
    for (l = kMuseLambdaMinX; l <= lambda1 - d1; l += d1) {
      double sens = cpl_polynomial_eval_1d(fit1, l, NULL),
             /* error that doubles every 50 Angstrom */
             serr = 2. * (lambda1 - l) / 50. * serr1 + serr1;
      if (sens <= 0) {
        cpl_table_set_invalid(tsens, "lambda", irow++);
        cpl_msg_debug(__func__, "invalid blueward extrapolation: %.3f %f +/- %f",
                      l, sens, serr);
        continue;
      }
      cpl_table_set_double(tsens, "lambda", irow, l);
      cpl_table_set_double(tsens, "sens", irow, sens);
      cpl_table_set_double(tsens, "serr", irow, serr);
      cpl_table_set_int(tsens, "dq", irow++, (int)EURO3D_OUTSDRANGE);
    } /* for l */
    cpl_msg_debug(__func__, "Extrapolated blue end: %.1f...%.1f Angstrom (using"
                  " data from %.1f...%.1f Angstrom)", kMuseLambdaMinX,
                  lambda1 - d1, lambda1, lambda1 + aDistance);
  } /* if blue end */
  if (fit2) { /* extrapolate red end */
    for (l = lambda2 + d2; fit2 && l <= kMuseLambdaMaxX; l += d2) {
      double sens = cpl_polynomial_eval_1d(fit2, l, NULL),
             serr = 2. * (l - lambda2) / 50. * serr2 + serr2;
      if (sens <= 0) {
        cpl_table_set_invalid(tsens, "lambda", irow++);
        cpl_msg_debug(__func__, "invalid redward extrapolation: %.3f %f +/- %f",
                      l, sens, serr);
        continue;
      }
      cpl_table_set_double(tsens, "lambda", irow, l);
      cpl_table_set_double(tsens, "sens", irow, sens);
      cpl_table_set_double(tsens, "serr", irow, serr);
      cpl_table_set_int(tsens, "dq", irow++, (int)EURO3D_OUTSDRANGE);
    } /* for l */
    cpl_msg_debug(__func__, "Extrapolated red end: %.1f...%.1f Angstrom (using "
                  "data from %.1f...%.1f Angstrom)", lambda2 + d2,
                  kMuseLambdaMaxX, lambda2 - aDistance, lambda2);
  } /* if red end */
#if 0
  cpl_polynomial_dump(fit1, stdout);
  cpl_polynomial_dump(fit2, stdout);
  fflush(stdout);
#endif
  cpl_polynomial_delete(fit1);
  cpl_polynomial_delete(fit2);
  /* clean up invalid entries */
  cpl_table_select_all(tsens);
  cpl_table_and_selected_invalid(tsens, "sens");
  cpl_table_erase_selected(tsens);
  /* sort the resulting table again */
  cpl_table_sort(tsens, order);
  cpl_propertylist_delete(order);
  return CPL_ERROR_NONE;
} /* muse_flux_response_extrapolate() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Compare measured flux distribution over wavelength with calibrated
          stellar fluxes and derive instrumental sensitivity curve.
  @param  aFluxObj     image containing the standard flux measurements
  @param  aSelect      how to select the standard star
  @param  aAirmass     the corresponding airmass (passing 0.0 is allowed to
                       switch off extinction correction)
  @param  aReference   table containing the reference response for the star
  @param  aTellBands   table containing the telluric band regions (optional)
  @param  aExtinct     the extinction table
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  Select the star in aFluxObj->intimage depending on aSelect, to be either the
  brightest or the nearest integrated object.
  @note If aSelect is MUSE_FLUX_SELECT_NEAREST, then aFluxObj->raref and
        aFluxObj->decref need to be filled (i.e. not NAN) with the coordinates
        of the real reference object on sky, otherwise
        MUSE_FLUX_SELECT_BRIGHTEST is assumed.

  Compare the measured flux at each wavelength to the reference flux, and scale
  by airmass, to create the sensitivity function. Questionable entries (those
  within the telluric regions and outside the wavelength range of the MUSE
  setup), are marked, the ones outside subsequently removed. The telluric
  regions are interpolated over (using a 2nd order polynomial) or extrapolated
  (linearly), and telluric correction factors are computed for these regions,
  scaled by the airmass of the standard star exposure. The resulting curve is
  then extrapolated linearly to cover the wavelength range that MUSE can
  possibly cover with any pixel.

  The real result of this function is the sensitivity table that gets added to
  aFluxObj.
  @note The sensitivity distribution is not smoothed or fit in any way, so it
        will contain outliers!

  The sensitivity table produced here can be converted to the necessary
  STD_RESPONSE and STD_TELLURIC tables using muse_flux_get_response_table() and
  muse_flux_get_telluric_table().

  @note The reference table aReference will be duplicated as aFluxObj->reference
        on success.

  @error{return CPL_ERROR_NULL_INPUT,
         inputs aFluxObj\, its intimage component\, or aReference table are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aAirmass invalid (< 1.)}
  @error{use builtin defaults, aTellBands is NULL}
  @error{output warning\, but continue\, ignoring extinction,
         extinction table is missing}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_response_compute(muse_flux_object *aFluxObj,
                           muse_flux_selection_type aSelect, double aAirmass,
                           const cpl_table *aReference,
                           const cpl_table *aTellBands,
                           const cpl_table *aExtinct)
{
  cpl_ensure_code(aFluxObj && aFluxObj->intimage && aReference,
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aAirmass >= 1., CPL_ERROR_ILLEGAL_INPUT);
  if (!aExtinct) {
    cpl_msg_warning(__func__, "Extinction table not given!");
  }
  if (aSelect == MUSE_FLUX_SELECT_NEAREST &&
      (!isfinite(aFluxObj->raref) || !isfinite(aFluxObj->decref))) {
    cpl_msg_warning(__func__, "Reference position %f,%f contains infinite "
                    "values, using flux to select star!", aFluxObj->raref,
                    aFluxObj->decref);
    aSelect = MUSE_FLUX_SELECT_BRIGHTEST;
  }
  muse_flux_response_set_telluric_bands(aFluxObj, aTellBands);

  int nobjects = cpl_image_get_size_y(aFluxObj->intimage->data);
  const char *bunit = muse_pfits_get_bunit(aFluxObj->intimage->header);
  /* find brightest and nearest star */
  double flux = 0., dmin = DBL_MAX;
  int n, nstar = 1, nstardist = 1;
  for (n = 1; n <= nobjects; n++) {
    char kw[KEYWORD_LENGTH];
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_RA, n);
    double ra = cpl_propertylist_get_double(aFluxObj->intimage->header, kw);
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_DEC, n);
    double dec = cpl_propertylist_get_double(aFluxObj->intimage->header, kw),
           dthis = muse_astro_angular_distance(ra, dec, aFluxObj->raref,
                                               aFluxObj->decref);
    cpl_msg_debug(__func__, "distance(%d) = %f arcsec", n, dthis * 3600.);
    if (fabs(dthis) < dmin) {
      dmin = dthis;
      nstardist = n;
    }
    snprintf(kw, KEYWORD_LENGTH, MUSE_HDR_FLUX_OBJn_FLUX, n);
    double this = cpl_propertylist_get_double(aFluxObj->intimage->header, kw);
    cpl_msg_debug(__func__, "flux(%d) = %e %s", n, this, bunit);
    if (this > flux) {
      flux = this;
      nstar = n;
    }
  } /* for n (all objects) */
  int nselected;
  char *outstring = NULL,
       *comment = NULL;
  if (aSelect == MUSE_FLUX_SELECT_BRIGHTEST) {
    outstring = cpl_sprintf("Selected the brightest star (%d of %d; %.3e %s)"
                            " as reference object", nstar, nobjects, flux, bunit);
    comment = cpl_sprintf(MUSE_HDR_FLUX_NSEL_C, "brightest");
    nselected = nstar;
  } else {
    outstring = cpl_sprintf("Selected the nearest star (%d of %d; %.2f arcsec) "
                            "as reference object", nstar, nobjects, dmin*3600.);
    comment = cpl_sprintf(MUSE_HDR_FLUX_NSEL_C, "nearest");
    nselected = nstardist;
  }
  cpl_msg_info(__func__, "%s", outstring);
  cpl_free(outstring);

  /* add selected object to the header of the intimage, *
   * use the comment to give the reason                 */
  cpl_propertylist_append_int(aFluxObj->intimage->header, MUSE_HDR_FLUX_NSEL,
                              nselected);
  cpl_propertylist_set_comment(aFluxObj->intimage->header, MUSE_HDR_FLUX_NSEL,
                               comment);
  cpl_free(comment);

  /* table of sensitivity function, its sigma, and a Euro3D-like quality flag */
  muse_flux_response_sensitivity(aFluxObj,
                                 nselected - 1, aReference,
                                 aAirmass, aExtinct);
  muse_flux_response_dump_sensitivity(aFluxObj, "initial");

  muse_flux_response_mark_questionable(aFluxObj);
  muse_flux_response_dump_sensitivity(aFluxObj, "intermediate");
  /* remove the ones outside the wavelength range */
  cpl_table_select_all(aFluxObj->sensitivity);
  cpl_table_and_selected_int(aFluxObj->sensitivity, "dq", CPL_EQUAL_TO,
                             (int)EURO3D_OUTSDRANGE);
  cpl_table_erase_selected(aFluxObj->sensitivity);
  muse_flux_response_dump_sensitivity(aFluxObj, "intercut");

  /* interpolate telluric (dq == EURO3D_TELLURIC) regions *
   * and compute telluric correction factor               */
  muse_flux_response_telluric(aFluxObj, aAirmass);
  muse_flux_response_dump_sensitivity(aFluxObj, "interpolated");
  /* extend the wavelength range using linear extrapolation */
  double width = 150.; /* use 150 Angstrom to fit line */
  if (cpl_propertylist_has(aFluxObj->intimage->header, MUSE_HDR_FLUX_FFCORR)) {
    /* The flat-field corrected curve has a sharp edge at the blue end so    *
     * that we can only use 15 Angstrom max to fit the linear extrapolation  *
     * function to the data; at the red end this change is irrelevant, since *
     * it's already smooth due to the correction of the telluric feature.    */
    width = 15.;
  }
  muse_flux_response_extrapolate(aFluxObj, width);
  muse_flux_response_dump_sensitivity(aFluxObj, "extrapolated");

  /* store the used reference table in the flux-object, since we *
   * are about to return with an error code indicating success   */
  aFluxObj->reference = cpl_table_duplicate(aReference);

  return CPL_ERROR_NONE;
} /* muse_flux_response_compute() */

/*----------------------------------------------------------------------------*/
/**
 * @brief MUSE response table definition.
 *
 * A MUSE response table has the following columns:
 * - 'lambda': the wavelength in Angstrom
 * - "response": instrument response derived from standard star
 * - "resperr": instrument response error derived from standard star
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_flux_responsetable_def[] = {
  { "lambda", CPL_TYPE_DOUBLE, "Angstrom", "%7.2f", "wavelength", CPL_TRUE },
  { "response", CPL_TYPE_DOUBLE,
    "2.5*log10((count/s/Angstrom)/(erg/s/cm**2/Angstrom))", "%.4e",
    "instrument response derived from standard star", CPL_TRUE  },
  { "resperr", CPL_TYPE_DOUBLE,
    "2.5*log10((count/s/Angstrom)/(erg/s/cm**2/Angstrom))", "%.4e",
    "instrument response error derived from standard star", CPL_TRUE  },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE  }
};

/*----------------------------------------------------------------------------*/
/**
 * @brief MUSE telluric correction table definition.
 *
 * A MUSE telluric correction table has the following columns:
 * - 'lambda': the wavelength in Angstrom
 * - "ftelluric": the telluric correction factor, normalized to an airmass of 1
 * - "ftellerr": the error of the telluric correction factor
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_flux_tellurictable_def[] = {
  { "lambda", CPL_TYPE_DOUBLE, "Angstrom", "%7.2f", "wavelength", CPL_TRUE  },
  { "ftelluric", CPL_TYPE_DOUBLE, "", "%.5f",
    "the telluric correction factor, normalized to an airmass of 1", CPL_TRUE  },
  { "ftellerr", CPL_TYPE_DOUBLE, "", "%.5f",
    "the error of the telluric correction factor", CPL_TRUE  },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE  }
};

/*----------------------------------------------------------------------------*/
/**
  @brief  Get the table of the standard star response function.
  @param  aFluxObj   MUSE flux object, containing integrated fluxes and the
                     calculated response
  @param  aSmooth    Smoothing option
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  Copy the response information from the sensitivity table of aFluxObj to a new
  table, also returned in the input aFluxObj.

  This function uses @ref muse_utils_spectrum_smooth() for smoothing the
  response curve. If any smoothing was applied to the table, the unsmoothed
  values are tracked in extra table columns (with "_unsmoothed" postfix).

  @error{return CPL_ERROR_NULL_INPUT,
         input flux object or its sensitivity components are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aSmooth is unknown}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_get_response_table(muse_flux_object *aFluxObj,
                             muse_spectrum_smooth_type aSmooth)
{
  cpl_ensure_code(aFluxObj && aFluxObj->sensitivity, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aSmooth <= MUSE_SPECTRUM_SMOOTH_PPOLY,
                  CPL_ERROR_ILLEGAL_INPUT);

  int nrow = cpl_table_get_nrow(aFluxObj->sensitivity);
  cpl_table *resp = muse_cpltable_new(muse_flux_responsetable_def, nrow);
  /* copy the relevant columns from the sensitivity table */
  const double *lambdas = cpl_table_get_data_double_const(aFluxObj->sensitivity,
                                                          "lambda"),
               *sens = cpl_table_get_data_double_const(aFluxObj->sensitivity,
                                                       "sens"),
               *serr = cpl_table_get_data_double_const(aFluxObj->sensitivity,
                                                       "serr");
  cpl_table_copy_data_double(resp, "lambda", lambdas);
  cpl_table_copy_data_double(resp, "response", sens);
  cpl_table_copy_data_double(resp, "resperr", serr);

  /* set the table in the flux object */
  aFluxObj->response = muse_table_new();
  aFluxObj->response->table = resp;
  /* base the header on the cube properties, but remove some stuff */
  if (aFluxObj->cube) {
    aFluxObj->response->header = cpl_propertylist_duplicate(aFluxObj->cube->header);
  } else {
    /* at least create an empty FITS header for the response table */
    aFluxObj->response->header = cpl_propertylist_new();
  }
  cpl_propertylist_erase_regexp(aFluxObj->response->header, MUSE_WCS_KEYS
                                "|^NAXIS|BUNIT", 0);
  /* the flat-spectrum correction status (MUSE_HDR_FLUX_FFCORR) should be *
   * set here since the cube creation propagates it from the pixel table, *
   * but there's no good way to check it here...                          */

  /* smooth the response curve */
  if (aSmooth != MUSE_SPECTRUM_SMOOTH_NONE) {
    /* keep the unsmoothed data in extra columns */
    cpl_table_duplicate_column(resp, "response_unsmoothed", resp, "response");
    cpl_table_duplicate_column(resp, "resperr_unsmoothed", resp, "resperr");
  }
  muse_utils_spectrum_smooth(aFluxObj->response, aSmooth);

  return CPL_ERROR_NONE;
} /* muse_flux_get_response_table() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Get the table of the telluric correction.
  @param  aFluxObj   MUSE flux object, containing integrated fluxes and the
                     calculated telluric correction
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  Copy the telluric correction factors from the sensitivity table of aFluxObj to
  a new table, also returned in the input aFluxObj. Pad telluric regions with
  extra entries of ftelluric = 1 (for later interpolation), remove all other
  invalid entries to minimize the size of the returned table.

  @note The table column "ftellerr" currently contains an error that is at most
        0.1, smaller than the distance between the telluric correction factor
        and 1., but 1e-4 as a mininum.

  @error{return CPL_ERROR_NULL_INPUT,
         input flux object or its sensitivity components are NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_get_telluric_table(muse_flux_object *aFluxObj)
{
  cpl_ensure_code(aFluxObj && aFluxObj->sensitivity, CPL_ERROR_NULL_INPUT);
  cpl_table *tsens = aFluxObj->sensitivity;
  int nrow = cpl_table_get_nrow(tsens);
  cpl_table *tell = muse_cpltable_new(muse_flux_tellurictable_def, nrow);
  /* copy the lambda and tellcor columns from the sensitivity table */
  cpl_table_fill_column_window_double(tell, "lambda", 0, nrow, 0);
  cpl_table_copy_data_double(tell, "lambda",
                             cpl_table_get_data_double_const(tsens, "lambda"));
  cpl_table_fill_column_window_double(tell, "ftelluric", 0, nrow, 0);
  cpl_table_copy_data_double(tell, "ftelluric",
                             cpl_table_get_data_double_const(tsens, "tellcor"));
  /* no (good) error estimates, use constant error as starting point */
#define TELL_MAX_ERR 0.1
#define TELL_MIN_ERR 1e-4
  cpl_table_fill_column_window_double(tell, "ftellerr", 0, nrow, TELL_MAX_ERR);

  /* duplicate the tellcor column, to get the invalidity info; *
   * pad telluric correction factors with 1. in the new table  */
  cpl_table_duplicate_column(tell, "tellcor", tsens, "tellcor");
  /* pad entries adjacent to the ones with a real telluric factor with 1 */
  cpl_table_unselect_all(tell);
  int irow;
  for (irow = 0; irow < nrow; irow++) {
    int err;
    cpl_table_get_double(tell, "tellcor", irow, &err); /* ignore value */
    if (err == 0) { /* has a valid entry -> do nothing */
      continue;
    }
    /* invalid entry, check previous one (again) */
    cpl_errorstate state = cpl_errorstate_get();
    double ftellcor = cpl_table_get_double(tell, "tellcor", irow - 1, &err);
    if (!cpl_errorstate_is_equal(state)) { /* recover from possible errors */
      cpl_errorstate_set(state);
    }
    if (err == 0 && ftellcor != 1.) { /* exist and is not 1 -> pad */
      cpl_table_set_double(tell, "ftelluric", irow, 1.);
      continue;
    }
    /* check the next one, too */
    state = cpl_errorstate_get();
    ftellcor = cpl_table_get_double(tell, "tellcor", irow + 1, &err);
    if (!cpl_errorstate_is_equal(state)) { /* recover from possible errors */
      cpl_errorstate_set(state);
    }
    if (err == 0 && ftellcor != 1.) { /* exist and is not 1 -> pad */
      cpl_table_set_double(tell, "ftelluric", irow, 1.);
      continue;
    }
    cpl_table_select_row(tell, irow); /* surrounded by invalid -> select */
  } /* for irow */
  cpl_table_erase_selected(tell); /* erase all the still invalid ones */
  cpl_table_erase_column(tell, "tellcor");

  /* next pass: adjust the error to be only about the distance between 1 and the value */
  nrow = cpl_table_get_nrow(tell);
  for (irow = 0; irow < nrow; irow++) {
    int err;
    double dftell = 1. - cpl_table_get_double(tell, "ftelluric", irow, &err),
           ftellerr = cpl_table_get_double(tell, "ftellerr", irow, &err);
    if (ftellerr > dftell) {
      ftellerr = fmax(dftell, TELL_MIN_ERR);
      cpl_table_set_double(tell, "ftellerr", irow, ftellerr);
    }
  } /* for irow */

  aFluxObj->telluric = muse_table_new();
  aFluxObj->telluric->table = tell;
  /* base the header on the cube properties, but remove some stuff */
  aFluxObj->telluric->header = cpl_propertylist_duplicate(aFluxObj->cube->header);
  cpl_propertylist_erase_regexp(aFluxObj->telluric->header, MUSE_WCS_KEYS
                                "|^NAXIS|BUNIT", 0);
  /* again, flat-spectrum correction status should be transferred by this */
  return CPL_ERROR_NONE;
} /* muse_flux_get_telluric_table() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Compute QC parameters, related to on-sky throughput.
  @param  aFluxObj   MUSE flux object, containing integrated fluxes and the
                     calculated telluric correction
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  This uses approximately known parameters of the VLT to compute the throughput
  of MUSE + VLT + atmosphere using the parameters available in the aFluxObj
  structure.

  If available, the final response curve (aFluxObj->response) is used. Then the
  QC parameters are also written to the output FITS header of the response.
  Otherwise, the (unsmoothed) sensitivity function (aFluxObj->sensitivity) is
  used, and the resulting values are just printed to screen/log.
  In both cases, a new table column "throughput" is created, or its contents are
  overwritten, if the column already exists.

  The keywords written (if aFluxObj contains the response curve) are QC_STD_THRU
  for the throughput values and QC_STD_NAME for the name of the standard star
  (field) that gets transferred from the target name in the cube header.

  @error{set and return CPL_ERROR_NULL_INPUT,
         input flux object both its sensitivity and response components are NULL}
  @error{set and return CPL_ERROR_DATA_NOT_FOUND,
         neither sensitivity nor response->table component could be duplicated}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_compute_qc(muse_flux_object *aFluxObj)
{
  cpl_ensure_code(aFluxObj && (aFluxObj->sensitivity || aFluxObj->response),
                  CPL_ERROR_NULL_INPUT);
  cpl_boolean hasresp = aFluxObj->response != NULL;

  /* if we have the headers, we can transfer OBS.TARG.NAME *
   * to a QC parameter to hold the star's name             */
  if (hasresp && aFluxObj->response->header && aFluxObj->cube &&
      aFluxObj->cube->header) {
    cpl_errorstate state = cpl_errorstate_get();
    const char *name = muse_pfits_get_targname(aFluxObj->cube->header);
    if (!name) {
      cpl_msg_warning(__func__, "Unknown standard star in exposure (missing "
                      "OBS.TARG.NAME)!");
      cpl_errorstate_set(state);
      name = "UNKNOWN";
    }
    cpl_propertylist_update_string(aFluxObj->response->header, QC_STD_NAME,
                                   name);
  } /* if reponse header */

  /* shortcut to the relevant table,                           *
   * modify it by appending a throughput column ("throughput") */
  cpl_table *thruput = hasresp ? aFluxObj->response->table
                               : aFluxObj->sensitivity;
  cpl_ensure_code(thruput, CPL_ERROR_DATA_NOT_FOUND);

  cpl_msg_info(__func__, "Computing throughput using effective VLT area of %.1f"
               " cm**2, from %s curve", kVLTArea,
               hasresp ? "smoothed response" : "unsmoothed sensitivity");

  const double h = CPL_PHYS_H * 1e7, /* h in cgs units */
               c = CPL_PHYS_C * 1e10; /* c in Angstrom */
  if (!cpl_table_has_column(thruput, "throughput")) {
    cpl_table_new_column(thruput, "throughput", CPL_TYPE_DOUBLE);
  }
  /* easier done in a loop than with CPL table functions... */
  int i, n = cpl_table_get_nrow(thruput);
  for (i = 0; i < n; i++) {
    int err1, err2;
    double lambda = cpl_table_get(thruput, "lambda", i, &err1),
           sens = cpl_table_get(thruput, hasresp ? "response" : "sens", i, &err2);
    if (err1 || err2) {
      cpl_table_set_invalid(thruput, "throughput", i);
      continue;
    }
    double value = h * c / kVLTArea / lambda * pow(10., 0.4 * sens);
#if 0
    cpl_msg_debug(__func__, "%.3f %.3f --> %e, %e ==> %.3f", lambda,
                  sens, h * c / kVLTArea / lambda,  pow(10., 0.4 * sens),
                  value);
#endif
    cpl_table_set_double(thruput, "throughput", i, value);
  } /* for i (all table rows) */

  /* average the throughput curve at +/- 100 Angstrom around five points */
  cpl_msg_indent_more();
  float lambda;
  for (lambda = 5000.; lambda < 9100.; lambda += 1000.) {
    if (fabs(lambda - 6000.) < 0.1) {
      /* skip computation at 6000 because this wavelength *
       * range can be affected by the NaD notch filter    */
      continue;
    }
    cpl_table_unselect_all(thruput);
    cpl_table_or_selected_double(thruput, "lambda", CPL_NOT_LESS_THAN,
                                 lambda - 100.);
    cpl_table_and_selected_double(thruput, "lambda", CPL_NOT_GREATER_THAN,
                                 lambda + 100.);
    cpl_table *t = cpl_table_extract_selected(thruput);
    double mean = cpl_table_get_column_mean(t, "throughput"),
           stdev = cpl_table_get_column_stdev(t, "throughput");
    cpl_msg_info(__func__, "Throughput at %.0f +/- 100 Angstrom: %.4f +/- %.4f",
                 lambda, mean, stdev);
    cpl_table_delete(t);

    /* save as QC parameters in the response table header (if created) */
    if (hasresp && aFluxObj->response->header) {
      char *kw = cpl_sprintf(QC_STD_THRU, lambda);
      cpl_propertylist_update_float(aFluxObj->response->header, kw, mean);
      cpl_free(kw);
    } /* if reponse header */
  } /* for lambda */
  cpl_msg_indent_less();

  return CPL_ERROR_NONE;
} /* muse_flux_compute_qc() */

/*----------------------------------------------------------------------------*/
/**
  @brief  Compute QC zeropoint for given filter.
  @param  aFluxObj   MUSE flux object, containing integrated fluxes and the
                     calculated telluric correction
  @param  aFilter    table contining the filter's throughput curve
  @param  aName      the name of the filter
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  This function integrates the aFluxObj->reference curve of the standard star
  over the filter function to compute its magnitude. Then, the final output
  response (aFluxObj->response, if available) or the unsmoothed sensitivity
  (aFluxObj->sensitivity) is used to create the measured spectrum of the
  standard star and integrate this as well. The zeropoint then is the difference
  between the two:

     ZP = m_obs - m_ref = -2.5 log10(F_obs / F_ref)

  @note The input argument aName is used to create the keyword of the output
        QC parameter. It will be the part of the string after the last '_' in
        the filter's name. If NULL or aName does not contain an underscore,
        "UNKNOWN" will be used.

  @error{set and return CPL_ERROR_NULL_INPUT,
         input flux object both its sensitivity and response components\, reference spectrum\, aFilter\, or its table component are NULL}
  @error{set filter name for output FITS keyword to "UNKNOWN" but continue,
         aName is NULL or does not contain an underscore}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_compute_qc_zp(muse_flux_object *aFluxObj, const muse_table *aFilter,
                        const char *aName)
{
  cpl_ensure_code(aFluxObj && (aFluxObj->sensitivity || aFluxObj->response) &&
                  aFluxObj->reference && aFilter && aFilter->table,
                  CPL_ERROR_NULL_INPUT);

  /* use shortnames for the filters */
  char *p = aName ? strrchr(aName, '_') : NULL;
  const char *fshort = NULL;
  if (p) {
    fshort = p + 1;
  } else {
    fshort = "UNKNOWN";
    cpl_msg_warning(__func__, "%s filter given for QC zeropoint computation!",
                    fshort);
  }
  char *kw = cpl_sprintf(QC_STD_ZP, fshort); /* FITS keyword name */

  /* shortcut to response curve, prefer the (smoothed!) final version */
  cpl_boolean hasresp = aFluxObj->response != NULL;
  cpl_table *thruput  = hasresp ? aFluxObj->response->table
                                : aFluxObj->sensitivity;
  const double h = CPL_PHYS_H * 1e7, /* h in cgs units */
               c = CPL_PHYS_C * 1e10; /* c in Angstrom */

  /* Now compute the weighted sum of the (smoothed) response curve    *
   * over the filter function, which is the zeropoint in this filter. */
  double fref = 0., /* summed flux of the reference spectrum */
         fobs = 0.; /* summed flux of the observed spectrum */
  int i, n = cpl_table_get_nrow(thruput);
  for (i = 0; i < n; i++) {
    int err1, err2;
    double lambda = cpl_table_get(thruput, "lambda", i, &err1),
           sens = cpl_table_get(thruput, hasresp ? "response" : "sens", i, &err2),
           ref = muse_flux_response_interpolate(aFluxObj->reference, lambda,
                                                NULL, MUSE_FLUX_RESP_STD_FLUX);
    if (err1 || err2) {
      continue;
    }
    double fweight = muse_flux_response_interpolate(aFilter->table, lambda, NULL,
                                                    MUSE_FLUX_RESP_FILTER);

    /* recompute (smoothed) measured value at this wavelength, add it up */
    fobs += pow(10., 0.4 * sens) /* convert back to count (e-) */
          * ref                  /* take out the reference spectrum */
          * fweight              /* weight by the filter function */
          * h * c / kVLTArea / lambda; /* convert to f-lambda */
    /* the weighted reference flux */
    fref += ref * fweight;
  } /* for i (all thruput table rows) */

  /* no normalization by the total filter-weight is necessary, *
   * as we used the same weights in numerator and denominator  */
  double zp = -2.5 * log10(fobs / fref); /* zeropoint is in magnitudes */
  cpl_msg_indent_more();
  cpl_msg_info(__func__, "Zeropoint in filter %s: %.3f mag (throughput %.3f)",
               fshort, zp, pow(10., -0.4 * zp));
  cpl_msg_indent_less();
  if (aFluxObj->response->header) {
    /* finally add the QC keyword to the header */
    cpl_propertylist_update_float(aFluxObj->response->header, kw, zp);
  }
  cpl_free(kw);

  return CPL_ERROR_NONE;
} /* muse_flux_compute_qc_zp() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert the input pixel table from counts to fluxes.
  @param   aPixtable     the input pixel table containing the exposure to be
                         flux calibrated
  @param   aResponse     the tabulated response curve with header
  @param   aExtinction   the extinction curve
  @param   aTelluric     the telluric correction table with header
  @return  CPL_ERROR_NONE for success, any other cpl_error_code for failure.
  @remark  The resulting correction is directly applied and returned in the
           input pixel table.
  @remark This function adds a FITS header (@ref MUSE_HDR_PT_FLUXCAL) with the
          boolean value 'T' to the pixel table, for information.

  Loop through all pixels in the input pixel table, evaluate the response
  polynomial at the corresponding wavelength and multiply by the response
  factor. Treat the variance accordingly. Also multiply by the telluric
  correction factor, weighted by the airmass, for wavelengths redward of the
  start of the telluric absorption regions.

  @qa Apply the flux calibration to the standard star exposure on which it
      was derived and compare the reference spectra with the flux-calibrated
      spectrum of the respective star.

  @error{return CPL_ERROR_NULL_INPUT,
         the input pixel table\, its header\, or the response table are NULL}
  @error{return CPL_ERROR_CONTINUE,
         the input pixel table data unit is not "count"}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         the input pixel table data is already flux calibrated}
  @error{return CPL_ERROR_TYPE_MISMATCH,
         the flat-field spectrum correction status between aPixtable and aResponse does not match}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         response curve and pixel table were created using different instrument modes}
  @error{output warning only\, but continue,
         the flat-field spectrum correction status between aPixtable and aTelluric does not match}
  @error{output warning\, return CPL_ERROR_ILLEGAL_INPUT,
         the input pixel table data has non-positive exposure time}
  @error{output warning and reset airmass to 0. to not do extinction correction,
         airmass value cannot be determined}
  @error{output warning and continue without extinction correction,
         the extinction curve is missing}
  @error{output info message and continue without telluric correction,
         the telluric correction table is missing}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_flux_calibrate(muse_pixtable *aPixtable, const muse_table *aResponse,
                    const cpl_table *aExtinction, const muse_table *aTelluric)
{
  cpl_ensure_code(aPixtable && aPixtable->header && aResponse,
                  CPL_ERROR_NULL_INPUT);
  const char *unitdata = cpl_table_get_column_unit(aPixtable->table,
                                                   MUSE_PIXTABLE_DATA);
  cpl_ensure_code(unitdata && strncmp(unitdata, kMuseFluxUnitString,
                                      strlen(kMuseFluxUnitString)),
                  CPL_ERROR_CONTINUE); /* already flux calibrated */
  cpl_ensure_code(unitdata && !strncmp(unitdata, "count", 6),
                  CPL_ERROR_BAD_FILE_FORMAT);

  /* the code below needs only the table-component of the *
   * response curve and the telluric correction spectrum  */
  cpl_table *response = NULL,
            *telluric = NULL;
  /* get the tag of the pixel table for possible outputs */
  const char *catg = muse_pfits_get_pro_catg(aPixtable->header);
#define FF_MSG "(flat-field spectrum %scorrected)"
  if (aResponse) {
    response = aResponse->table;
    /* cross-check the header of the response curve against *
     * the flat-field spectrum status of the pixel table    */
    cpl_boolean respff = cpl_propertylist_has(aResponse->header,
                                              MUSE_HDR_FLUX_FFCORR),
                ptff = cpl_propertylist_has(aPixtable->header,
                                            MUSE_HDR_PT_FFCORR);
    if ((respff && !ptff) || (!respff && ptff)) {
      return cpl_error_set_message(__func__, CPL_ERROR_TYPE_MISMATCH,
                                   "Cannot apply this %s "FF_MSG" to this %s "FF_MSG,
                                   MUSE_TAG_STD_RESPONSE, !respff ? "un" : "",
                                   catg, !ptff ? "un" : "");
    } /* if unmatched ff status */

    /* check for instrument mode */
    cpl_errorstate state = cpl_errorstate_get();
    muse_ins_mode moder = muse_pfits_get_mode(aResponse->header),
                  moded = muse_pfits_get_mode(aPixtable->header);
    if (cpl_errorstate_is_equal(state) && moder != moded) {
      const char *insmoder = muse_pfits_get_insmode(aResponse->header),
                 *insmoded = muse_pfits_get_insmode(aPixtable->header);
      return cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT,
                                   "Cannot apply %s (%s) to dataset (%s)!",
                                   MUSE_TAG_STD_RESPONSE, insmoder, insmoded);
    }
    if (!cpl_errorstate_is_equal(state)) {
      /* one object does not have an instrument mode, only *
       * occurs during testing, so swallow the state       */
      cpl_errorstate_set(state);
    }
  } /* if aResponse */
  double airmass = muse_astro_airmass(aPixtable->header);
  if (airmass < 0) {
    cpl_msg_warning(__func__, "Airmass unknown, not doing extinction "
                    "correction: %s", cpl_error_get_message());
    /* reset to zero so that it has no effect */
    airmass = 0.;
  }
  if (aTelluric) {
    /* duplicate the telluric correction table to apply the airmass correction */
    telluric = cpl_table_duplicate(aTelluric->table);
    /* use negative exponent to be able to multiply (instead of divide) *
     * by the correction factor (should be slightly faster)             */
    cpl_table_power_column(telluric, "ftelluric", -airmass);
    /* not critical, but still cross-check flat-field spectrum status here */
    cpl_boolean tellff = cpl_propertylist_has(aTelluric->header,
                                              MUSE_HDR_FLUX_FFCORR),
                ptff = cpl_propertylist_has(aPixtable->header,
                                            MUSE_HDR_PT_FFCORR);
    if ((tellff && !ptff) || (!tellff && ptff)) {
      cpl_msg_warning(__func__, "Applying %s "FF_MSG" to %s "FF_MSG", this may "
                      "not be correct!", MUSE_TAG_STD_TELLURIC,
                      !tellff ? "un" : "", catg, !ptff ? "un" : "");
    } /* if unmatched ff status */
  } /* if aTelluric */

  /* warn for non-critical failure */
  if (!aExtinction) {
    cpl_msg_warning(__func__, "%s missing!", MUSE_TAG_EXTINCT_TABLE);
  }

  double exptime = muse_pfits_get_exptime(aPixtable->header);
  if (exptime <= 0.) {
    cpl_msg_warning(__func__, "Non-positive EXPTIME, not doing flux calibration!");
    cpl_table_delete(telluric);
    return CPL_ERROR_ILLEGAL_INPUT;
  }

  cpl_msg_info(__func__, "Starting flux calibration (exptime=%.2fs, airmass=%.4f),"
               " %s telluric correction", exptime, airmass,
               aTelluric ? "with" : "without ("MUSE_TAG_STD_TELLURIC" not given)");
  float *lambda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
  cpl_size i, nrow = muse_pixtable_get_nrow(aPixtable);
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aExtinction, airmass, data, exptime, lambda, nrow, response,  \
                 stat, telluric)
  for (i = 0; i < nrow; i++) {
    /* double values for intermediate results of this row */
    double ddata = data[i], dstat = stat[i];

    /* correct for extinction */
    if (aExtinction) {
      double fext = pow(10., 0.4 * airmass
                             * muse_flux_response_interpolate(aExtinction,
                                                              lambda[i], NULL,
                                                              MUSE_FLUX_RESP_EXTINCT));
#if 0
      printf("%f, data/stat = %f/%f -> ", fext, ddata, dstat);
#endif
      ddata *= fext;
      dstat *= (fext * fext);
#if 0
      printf(" --> %f/%f\n", ddata, dstat), fflush(NULL);
#endif
    }

    /* the difference in lambda coverage per pixel seems to be  *
     * corrected for by the flat-field, so assuming a constant  *
     * value for all pixels seems to be the correct thing to do */
    double dlambda = kMuseSpectralSamplingA,
           dlamerr = 0.02, /* assume typical fixed error for the moment */
           /* resp/rerr get returned in mag units as in the table */
           rerr, resp = muse_flux_response_interpolate(response, lambda[i],
                                                       &rerr,
                                                       MUSE_FLUX_RESP_FLUX);
    /* convert from 2.5 log10(x) to non-log flux units */
    resp = pow(10., 0.4 * resp);
    /* magerr = 2.5 / log(10) * error / flux     (see IRAF phot docs) *
     * ==> error = magerr / 2.5 * log(10) * flux                      */
    rerr = rerr * CPL_MATH_LN10 * resp / 2.5;
#if 0
    printf("%f/%f/%f, %e/%e, data/stat = %e/%e -> ", lambda[i], dlambda, dlamerr, resp, rerr,
           ddata, dstat);
#endif
    dstat = dstat * pow((1./(resp * exptime * dlambda)), 2)
          + pow(ddata * rerr / (resp*resp * exptime * dlambda), 2)
          + pow(ddata * dlamerr / (resp * exptime * dlambda*dlambda), 2);
    ddata /= (resp * exptime * dlambda);
#if 0
    printf("%e/%e\n", ddata, dstat), fflush(NULL);
#endif

    /* now convert to the float values to be stored in the pixel table,   *
     * scaled by kMuseFluxUnitFactor to keep the floats from underflowing */
    ddata *= kMuseFluxUnitFactor;
    dstat *= kMuseFluxStatFactor;

    /* do the telluric correction, if the wavelength is redward of the start *
     * of the telluric regions, and if a telluric correction was given       */
    if (lambda[i] < kTelluricBands[0][0] || !telluric) {
      data[i] = ddata;
      stat[i] = dstat;
      continue; /* skip telluric correction in the blue */
    }
    double terr, tell = muse_flux_response_interpolate(telluric, lambda[i],
                                                       &terr,
                                                       MUSE_FLUX_TELLURIC);
    data[i] = ddata * tell;
    stat[i] = tell*tell * dstat + ddata*ddata * terr*terr;
  } /* for i (pixel table rows) */
  cpl_table_delete(telluric); /* NULL check done there... */

  /* now set the table column headers reflecting the flux units used */
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_DATA,
                            kMuseFluxUnitString);
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_STAT,
                            kMuseFluxStatString);

  /* add the status header */
  cpl_propertylist_update_bool(aPixtable->header, MUSE_HDR_PT_FLUXCAL,
                               CPL_TRUE);
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_FLUXCAL,
                               MUSE_HDR_PT_FLUXCAL_COMMENT);
  return CPL_ERROR_NONE;
} /* muse_flux_calibrate() */

/**@}*/
