/* -*- 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-2014 European Southern Observatory
 *           (C) 2001 Enrico Marchetti, 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_dar.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_combine.h"
#include "muse_pfits.h"
#include "muse_quality.h"
#include "muse_utils.h"
#include "muse_wcs.h"

/*----------------------------------------------------------------------------*
 *                             Debugging/feature Macros                       *
 *----------------------------------------------------------------------------*/
#define DEBUG_MUSE_DARCORRECT 0   /* if 1, output debugging in muse_dar_correct() */
#define DEBUG_MUSE_DARCHECK 0     /* if 1, output debug stuff in muse_dar_check(), *
                                   * if > 1 also write files                       */
#define MUSE_DARCHECK_GRID_FITS 0 /* if 1, use grid-based fitting in muse_dar_check(); *
                                   * it is more robust and slightly more accurate, but *
                                   * about 10 times slower!                            */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_dar          DAR (Differential Atmospheric Refraction)
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the saturation pressure using the Owens calibration.
  @param   temp   temperature (in Kelvin)
  @return  the saturation pressure for the given temperature

  This function is currently also used when choosing the Filippenko formulae.
 */
/*----------------------------------------------------------------------------*/
static double
muse_dar_owens_saturation_pressure(double temp)
{
  return -10474.0 + 116.43*temp - 0.43284*temp*temp + 0.00053840*pow(temp,3);
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the two coefficients for the Owens method.
  @param   temp   temperature (in Kelvin)
  @param   rhum   the relative humidity (in %)
  @param   pres   atmospheric pressure (in hPa)
  @param   d1     coefficient 1
  @param   d2     coefficient 2
 */
/*----------------------------------------------------------------------------*/
static void
muse_dar_owens_coeffs(double temp, double rhum, double pres,
                      double *d1, double *d2)
{
  /* pressure helpers: saturation pressure */
  double ps = muse_dar_owens_saturation_pressure(temp),
         p2 = rhum * ps, /* water vapor pressure */
         p1 = pres - p2; /* dry air pressure */
  /* helpers/coefficients */
  *d1 = p1/temp * (1. + p1*(57.90e-8 - 9.3250e-4/temp + 0.25844/temp/temp));
  *d2 = p2/temp * (1. + p2*(1. + 3.7e-4*p2)
                        * (-2.37321e-3 + 2.23366/temp - 710.792/temp/temp
                           + 7.75141e4/pow(temp,3)));
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the refractive index for the given wavelength following
           Owens.
  @param   l    the wavelength (in um)
  @param   d1   coefficient 1
  @param   d2   coefficient 2
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
static inline double
muse_dar_owens_nrindex(double l, double d1, double d2)
{
  double lisq = 1./(l*l); /* inverse square of the wavelength */
  return ((2371.34 + 683939.7/(130. - lisq) + 4547.3/(38.9 - lisq)) * d1
          + (6487.31 + 58.058 * lisq - 0.71150 * lisq*lisq
             + 0.08851 * lisq*lisq*lisq) * d2) * 1.0e-8;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the refractive index for the given wavelength following
           Filippenko.
  @param   l    the wavelength (in um)
  @param   T    temperature (in degrees Celsius)
  @param   f    water vapor pressure (in mmHg)
  @param   P    atmospheric pressure (in mmHg)
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
static inline double
muse_dar_filippenko_nrindex(double l, double T, double f, double P)
{
  double lisq = 1./(l*l); /* inverse square of the wavelength */
  /* 10^6 [n(lambda) - 1] at standard environmental conditions, Eq. (1) */
  double nl = 64.328 + 29498.1 / (146 - lisq) + 255.4 / (41 - lisq);
  /* correction for non-standard conditions, Eq. (2) */
  nl *= P / 720.883 * (1 + (1.049 - 0.0157 * T) * 1e-6 * P) / (1 + 0.003661 * T);
  /* correction for water vapor, Eq. (3) */
  nl -= (0.0624 - 0.000680 * lisq) / (1 + 0.003661 * T) * f;
  /* convert to refractive index n(lambda) */
  nl = nl * 1e-6 + 1;
  return nl;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the refractive index for the given wavelength following
           Edlén.
  @param   l      the wavelength [um]
  @param   t      temperature [Celsius]
  @param   p      pressure [Pa]
  @param   pv     partial pressure [Pa]
  @return  the refractive index
 */
/*----------------------------------------------------------------------------*/
static inline double
muse_dar_edlen_nrindex(double l, double t, double p, double pv)
{
  /* define constants */
  const double A = 8342.54, B = 2406147,
               C = 15998, D = 96095.43,
               E = 0.601, F = 0.00972, G = 0.003661;
  double S = 1./(l*l), /* supposed to be "laser vacuum wavelength"?! XXX */
         /* intermediate result for p, pv, and t */
         n_5 = 1 + 1e-8 * (A + B / (130 - S) + C / (38.9 - S)),
         X = (1 + 1e-8 * (E - F * t) * p) / (1 + G * t),
         n_tp = 1 + p * (n_5 - 1) * X / D,
         n = n_tp - 10e-10 * (292.75 / (t + 273.15)) * (3.7345 - 0.0401 * S) * pv;
  return n;
} /* muse_dar_edlen_nrindex() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the refractive index for the given wavelength following
           Ciddor.
  @param   l      the wavelength [um]
  @param   T      temperature [K]
  @param   p      pressure [Pa]
  @param   xv     mole fraction
  @param   xCO2   CO2 concentration [umol / mol], use 450.0 for a good default
                  value
  @return  the refractive index

  The source of the value xCO2 = 450 is Birch et al., 1993, Metrologia 30, 7
  (see the routine p3d_darc_amplitude_iag in the p3d IFS reduction package).
 */
/*----------------------------------------------------------------------------*/
static inline double
muse_dar_ciddor_nrindex(double l, double T, double p, double xv, double xCO2)
{
  /* define constants */
  const double w0 = 295.235, w1 = 2.6422, w2 = -0.03238, w3 = 0.004028,
               k0 = 238.0185, k1 = 5792105, k2 = 57.362, k3 = 167917,
               a0 = 1.58123e-6, a1 = -2.9331e-8, a2 = 1.1043e-10,
               b0 = 5.707e-6, b1 = -2.051e-8,
               c0 = 1.9898e-4, c1 = -2.376e-6,
               d = 1.83e-11, e = -0.765e-8,
               p_R1 = 101325, T_R1 = 288.15,
               Z_a = 0.9995922115,
               rho_vs = 0.00985938,
               R = 8.314472, M_v = 0.018015;
  double t = T - 273.15, /* temperature [Celsius] */
         S = 1./(l*l), /* supposed to be "laser vacuum wavelength"?! XXX */
         /* intermediate results that depend on S */
         r_as = 1e-8 * ((k1 / (k0 - S)) + (k3 / (k2 - S))),
         r_vs = 1.022e-8 * (w0 + w1 * S + w2 * S*S + w3 * S*S*S),
         /* using the CO2 concentration, calculate intermediate values for CO2 */
         M_a = 0.0289635 + 1.2011e-8 * (xCO2 - 400),
         r_axs = r_as * (1 + 5.34e-7 * (xCO2 - 450)),
         /* compressibility and density components */
         Z_m = 1 - (p / T) * (a0 + a1 * t + a2 * t*t + (b0 + b1 * t) * xv
                              + (c0 + c1 * t) * xv*xv)
             + (p/T)*(p/T) * (d + e * xv*xv),
         rho_axs = p_R1 * M_a / (Z_a * R * T_R1),
         rho_v = xv * p * M_v / (Z_m * R * T),
         rho_a = (1 - xv) * p * M_a / (Z_m * R * T),
         n = 1 + (rho_a / rho_axs) * r_axs + (rho_v / rho_vs) * r_vs;
  return n;
} /* muse_dar_ciddor_nrindex() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Correct the pixel coordinates of all pixels of a given pixel table
           for differential atmospheric refraction (DAR).
  @param   aPixtable    the input pixel table to correct for DAR
  @param   aLambdaRef   the reference wavelength (in Angstrom)
  @return  CPL_ERROR_NONE on success another CPL error code on failure
  @remark  The resulting correction is directly applied to the input pixel
           table.
  @remark  All needed environmental parameters (temperature [°C] := 11.5,
           relative humidity [%] := 14.5, and atmospheric pressure [mbar]
           := 743.0; default values taken from the DAR IDL routine of Enrico
           Marchetti, ESO, January 2001) are taken from the FITS keywords
           passed in the muse_pixtable object, they exist in standard ESO FITS
           files as TEL.AMBI.TEMP, TEL.AMBI.RHUM, TEL.AMBI.PRES.START and
           TEL.AMBI.PRES.END. If the ambient site monitor was down when
           writing the files and these parameters are unknown, then the
           defaults are used.
  @remark  This function adds a FITS header (@ref MUSE_HDR_PT_DAR_NAME) with the
           value of aLambdaRef to the pixel table, so that other functions can
           detect, if DAR correction has already been carried out. If DAR
           correction was skipped due to a too low or too high aLambdaRef, this
           header is set to -1.
  @remark  This function adds four FITS headers MUSE_HDR_PT_PREDAR_[XY]{LO,HI}
           (see muse_pixtable.h) to store the original values of the pixel
           table limits, before calling muse_pixtable_compute_limits().

  Loop through all pixels of the input pixel table, compute the DAR offset for
  the wavelength difference with respect to the reference wavelength, and apply
  it to the coordinates, taking into account the instrument rotation angle on
  the sky and the parallactic angle at the time of the observations.

  Four algorithms are available:
  - "Filippenko": The algorithm from Filippenko, 1982 PASP 94, 715. This only
    uses the formula from Owens which converts relative humidity to water vapor
    pressure.
  - "Owens": the algorithm from Sandin et al., 2008 A&A 486, 545 (see Sect. 2.5
    of the DRL Design document for the formulae used) which is actually based on
    the diff_atm_refr.pro IDL script by Enrico Marchetti (ESO) who allowed to
    use it in a GPL program. The formulae originally come from J. C. Owens,
    1967 Applied Optics 6, 51 - 59.
  - "Edlen": the algorithm from B. Edlén, 1966 Metrologia 2, 71 - 80 and
    K. P. Birch & M. J. Downs, 1993 Metrologia 30, 155 - 162. See
    http://emtoolbox.nist.gov/Wavelength/Documentation.asp
    for all formulae used.
  - "Ciddor": the algorithm from Phillip E. Ciddor, 1996 Applied Optics 35,
    1566 - 1573. This method is supposed to be the most accurate way to compute
    the refractive index of air, with a validity range of at least 3000 to 17000
    Angstrom, and was chosen by the International Association of Geodesy as
    their standard (http://emtoolbox.nist.gov/Wavelength/Documentation.asp#EdlenorCiddor).
    See http://emtoolbox.nist.gov/Wavelength/Documentation.asp#AppendixA for all
    formulae used here.
  Use the environment variable MUSE_DAR_CORRECT_METHOD to switch the default
  from Filippenko to something else. They differ in the way the refractive index
  of air is computed from the environmental parameters.

  @qa In case the exposure contains a bright enough continuum source, the
      function muse_dar_check can be used to check the result: after resampling,
      all objects should be aligned along the wavelength direction. In case of
      "empty" fields, a validation is difficult.

  @error{return CPL_ERROR_NULL_INPUT,
         the input pixel table or its header is NULL}
  @error{output info message\, skip the correction\, return CPL_ERROR_NONE,
         the input reference wavelength is invalid (< 3500 or > 22000)}
  @error{output info message\, skip the correction\, return CPL_ERROR_NONE,
         the input pixel table was already corrected for DAR}
  @error{propagate CPL error, the airmass value cannot be determined}
  @error{propagate CPL error, the parallactic angle is missing from the header}
  @error{propagate CPL error, the position angle is missing from the header}
  @error{output warning\, use defaults\, propagate CPL errors about missing FITS headers,
         the environmental conditions are missing from header}
  @error{return CPL_ERROR_INVALID_TYPE,
         pixel table WCS type is neither MUSE_PIXTABLE_WCS_PIXEL nor MUSE_PIXTABLE_WCS_CELSPH}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_dar_correct(muse_pixtable *aPixtable, double aLambdaRef)
{
  cpl_ensure_code(aPixtable && aPixtable->header, CPL_ERROR_NULL_INPUT);
  if (aLambdaRef > 22000. || aLambdaRef < 3500.) {
    cpl_msg_info(__func__, "Reference lambda %.3f Angstrom: skipping DAR "
                 "correction", aLambdaRef);
    cpl_propertylist_update_double(aPixtable->header, MUSE_HDR_PT_DAR_NAME, -1.);
    cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_DAR_NAME,
                                 MUSE_HDR_PT_DAR_C_SKIP);
    return CPL_ERROR_NONE;
  }
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_NAME) &&
      cpl_propertylist_get_double(aPixtable->header, MUSE_HDR_PT_DAR_NAME) > 0.) {
    cpl_msg_info(__func__, "pixel table already corrected: skipping DAR "
                 "correction");
    return CPL_ERROR_NONE;
  }

  /* -------------------------------------------------------------- *
   * get environmental and instrumental values from the FITS header *
   * -------------------------------------------------------------- */
  double airmass = muse_astro_airmass(aPixtable->header);
  cpl_ensure_code(airmass >= 1., cpl_error_get_code());
  /* simple zenith distance in radians */
  double z = acos(1./airmass);

  /* compute the mean parallactic angle during exposure */
  cpl_errorstate prestate = cpl_errorstate_get();
  double parang = muse_astro_parangle(aPixtable->header);
  cpl_ensure_code(cpl_errorstate_is_equal(prestate), cpl_error_get_code());

  /* compute the position angle on the sky from the angles we have */
  prestate = cpl_errorstate_get();
  double posang = muse_astro_posangle(aPixtable->header);
  cpl_ensure_code(cpl_errorstate_is_equal(prestate), cpl_error_get_code());

  cpl_boolean isWFM = muse_pfits_get_mode(aPixtable->header)
                    <= MUSE_MODE_WFM_AO_N;
  if (!isWFM) {
    cpl_msg_warning(__func__, "For NFM instrument mode DAR correction should "
                    "not be needed!");
  }

  /* query values and set defaults in case we didn't get proper values */
  prestate = cpl_errorstate_get();
  double temp = muse_pfits_get_temp(aPixtable->header); /* temperature [Celsius] */
  if (cpl_errorstate_is_equal(prestate)) {
    temp += 273.15; /* T[K] */
  } else {
    cpl_msg_warning(__func__, "FITS header does not contain temperature: %s",
                    cpl_error_get_message());
    temp = 11.5 + 273.15; /* T[K] */
  }
  prestate = cpl_errorstate_get();
  double rhum = muse_pfits_get_rhum(aPixtable->header); /* relative humidity [%] */
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_msg_warning(__func__, "FITS header does not contain relative humidity: %s",
                    cpl_error_get_message());
    rhum = 14.5;
  }
  rhum /= 100.; /* convert from % to fraction */
  prestate = cpl_errorstate_get();
  double pres = (muse_pfits_get_pres_start(aPixtable->header) /* pressure [mbar] */
                 + muse_pfits_get_pres_end(aPixtable->header)) / 2.;
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_msg_warning(__func__, "FITS header does not contain pressure values: %s",
                    cpl_error_get_message());
    pres = 743.;
  }

  /* -------------------------------------------------------------------------- *
   * choose method and compute the refractive index at the reference wavelength *
   * -------------------------------------------------------------------------- */
  enum {
    MUSE_DAR_METHOD_FILIPPENKO = 0,
    MUSE_DAR_METHOD_OWENS,
    MUSE_DAR_METHOD_EDLEN,
    MUSE_DAR_METHOD_CIDDOR,
  };
  int method = MUSE_DAR_METHOD_FILIPPENKO;
  if (getenv("MUSE_DAR_CORRECT_METHOD") &&
      !strncmp(getenv("MUSE_DAR_CORRECT_METHOD"), "Owens", 6)) {
    method = MUSE_DAR_METHOD_OWENS;
  } else if (getenv("MUSE_DAR_CORRECT_METHOD") &&
      !strncmp(getenv("MUSE_DAR_CORRECT_METHOD"), "Edlen", 6)) {
    method = MUSE_DAR_METHOD_EDLEN;
  } else if (getenv("MUSE_DAR_CORRECT_METHOD") &&
      !strncmp(getenv("MUSE_DAR_CORRECT_METHOD"), "Ciddor", 7)) {
    method = MUSE_DAR_METHOD_CIDDOR;
  }
  /* compute refractive index for reference wavelength in um and *
   * output properties in "natural" (for the formulae) units     */
  double d1, d2,
         fp, /* water vapor pressure in mmHg */
         nr0, /* refractive index of air at reference wavelength */
         p = pres * 100, /* atmopheric pressure [Pa] */
         t = temp - 273.15, /* temperature [Celsius] */
         pv, /* partial vapor pressure */
         xv; /* mole fraction */
  if (method == MUSE_DAR_METHOD_OWENS) {
    muse_dar_owens_coeffs(temp, rhum, pres, &d1, &d2);
    nr0 = muse_dar_owens_nrindex(aLambdaRef / 10000., d1, d2);
    cpl_msg_info(__func__, "DAR for T=%.2f K, RH=%.2f %%, pres=%.1f mbar, "
                 "at airmass=%.4f, using ref. wavelength %.6f um (Owens)",
                 temp, rhum*100., pres, 1./cos(z), aLambdaRef / 10000.);
  } else if (method == MUSE_DAR_METHOD_EDLEN ||
             method == MUSE_DAR_METHOD_CIDDOR) {
    /* follow the detailed procedure from                                  *
     *    http://emtoolbox.nist.gov/Wavelength/Documentation.asp#AppendixA */
    /* Calculate saturation vapor pressure psv(t) over water,
     * equations (A1) to (A7) */
    const double T = temp, /* temperature [K] */
                 K1 = 1.16705214528E+03,
                 K2 = -7.24213167032E+05,
                 K3 = -1.70738469401E+01,
                 K4 = 1.20208247025E+04,
                 K5 = -3.23255503223E+06,
                 K6 = 1.49151086135E+01,
                 K7 = -4.82326573616E+03,
                 K8 = 4.05113405421E+05,
                 K9 = -2.38555575678E-01,
                 K10 = 6.50175348448E+02,
                 Omega = T + K9 / (T - K10),
                 A = Omega*Omega + K1 * Omega + K2,
                 B = K3 * Omega*Omega + K4 * Omega + K5,
                 C = K6 * Omega*Omega + K7 * Omega + K8,
                 X = -B + sqrt(B*B - 4 * A * C);
    double psv_w = 1e6 * pow(2 * C / X, 4);
    /* Calculate saturation vapor pressure psv(t) over ice, *
     * equations (A8) tl (A13)                              */
    const double A1 = -13.928169,
                 A2 = 34.7078238,
                 Theta = T / 273.16,
                 Y = A1 * (1 - pow(Theta, -1.5)) + A2 * (1 - pow(Theta, -1.25));
    double psv_i = 611.657 * exp(Y);
    /* determine Humidity */
    /* for the Edlén equation: for relative humidity RH [%], calculate      *
     * partial pressure using psw_w for t > 0 [Celsius] and psw_i for t < 0 */
    if (t > 0.) {
      pv = rhum * psv_w; /* use saturation pressure over water */
    } else {
      pv = rhum * psv_i; /* use saturation pressure over ice */
    }
    /* for the Ciddor equation: express humidity as a mole fraction */
    const double alpha = 1.00062,
                 beta = 3.14e-8,
                 gamma = 5.60e-7;
    double f = alpha + beta * p + gamma * t*t; /* enhancement factor f(p,t) */
    /* for relative humidity RH [%], calculate mole fraction xv using psw(t) */
    if (t > 0.) {
      xv = rhum * f * psv_w / p; /* use saturation pressure over water */
    } else {
      xv = rhum * f * psv_i / p; /* use saturation pressure over ice */
    }
    if (method == MUSE_DAR_METHOD_EDLEN) {
      nr0 = muse_dar_edlen_nrindex(aLambdaRef / 10000., t, p, pv);
    } else { /* MUSE_DAR_METHOD_CIDDOR */
      nr0 = muse_dar_ciddor_nrindex(aLambdaRef / 10000., T, p, xv, 450.);
    }
    cpl_msg_info(__func__, "DAR for T=%.2f degC, RH=%.2f %%, p=%.1f Pa, "
                 "at airmass=%.4f, using ref. wavelength %.6f um (%s)",
                 t, rhum*100., p, 1./cos(z), aLambdaRef / 10000.,
                 method == MUSE_DAR_METHOD_EDLEN ? "Edlen" : "Ciddor");
  } else {
#define CONVERT_hPa_TO_mmHg 0.75006158 /* conversion from hPa (or mbar) to *
                                        * mmHg, needed for Filippenko      */
    /* use the Owens formula to derive saturation pressure, still needs T[K] */
    double ps = muse_dar_owens_saturation_pressure(temp);
    /* using that, derive the water vapor pressure in mmHg */
    fp = rhum * ps * CONVERT_hPa_TO_mmHg;
    temp -= 273.15; /* temperature (again) in degrees Celsius */
    pres *= CONVERT_hPa_TO_mmHg; /* need the pressure in mmHg */
    nr0 = muse_dar_filippenko_nrindex(aLambdaRef / 10000., temp, fp, pres);
    cpl_msg_info(__func__, "DAR for T=%.2f degC, fp=%.3f mmHg, P=%.1f mmHg, "
                 "at airmass=%.4f, using ref. wavelength %.6f um (Filippenko)",
                 temp, fp, pres, 1./cos(z), aLambdaRef / 10000.);
  }

  /* ---------------------------------- *
   * compute the direction of the shift *
   * ---------------------------------- */
  /* xshift is in E-W direction for posang = 0, yshift is N-S */
  double xshift = -sin((parang + posang) * CPL_MATH_RAD_DEG),
         yshift = cos((parang + posang) * CPL_MATH_RAD_DEG);

  /* apply correction for spaxel size */
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  cpl_ensure_code(wcstype == MUSE_PIXTABLE_WCS_PIXEL ||
                  wcstype == MUSE_PIXTABLE_WCS_CELSPH, CPL_ERROR_INVALID_TYPE);
  double fscale = 3600.; /* assume scale in arcsec */
  if (wcstype == MUSE_PIXTABLE_WCS_CELSPH) {
    fscale = 1. / 3600; /* scale in degrees */
    double xscale, yscale;
    muse_wcs_get_scales(aPixtable->header, &xscale, &yscale);
    xshift /= -xscale; /* need to reverse the sign for RA */
    yshift /= yscale;
  } else { /* assume nominal spatial scale */
    if (isWFM) {
      xshift /= kMuseSpaxelSizeX_WFM;
      yshift /= kMuseSpaxelSizeY_WFM;
    } else {
      xshift /= kMuseSpaxelSizeX_NFM;
      yshift /= kMuseSpaxelSizeY_NFM;
    }
  }

  /* ------------------------------ *
   * apply correction to all pixels *
   * ------------------------------ */
  /* diff. refr. base in arcsec converted from radians (Filippenko does *
   * the conversion using x206265 which converts radians to arcsec)     */
  double dr0 = tan(z) * fscale * CPL_MATH_DEG_RAD;
#if DEBUG_MUSE_DARCORRECT
  double wl;
  for (wl = 4650.; wl <= 9300; wl += 50) {
    double nr;
    if (method == MUSE_DAR_METHOD_OWENS) {
      nr = muse_dar_owens_nrindex(wl / 10000., d1, d2);
    } else if (method == MUSE_DAR_METHOD_EDLEN) {
      nr = muse_dar_edlen_nrindex(wl / 10000., t, p, pv);
    } else if (method == MUSE_DAR_METHOD_CIDDOR) {
      nr = muse_dar_ciddor_nrindex(wl / 10000., temp, p, xv, 450.);
    } else {
      nr = muse_dar_filippenko_nrindex(wl / 10000., temp, fp, pres);
    }
    double dr = dr0 * (nr0 - nr);
    printf("%.1f Angstrom: %f ==> %f %f %s (%s)\n", wl, dr,
           xshift * dr, yshift * dr,
           wcstype == MUSE_PIXTABLE_WCS_CELSPH ? "deg" : "pix", __func__);
  }
  fflush(stdout);
#endif
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  cpl_size i, nmax = muse_pixtable_get_nrow(aPixtable);
  #pragma omp parallel for default(none)                                       \
          shared(d1, d2, dr0, fp, lbda, method, nmax, nr0, p, pres, pv, t,     \
                 temp, xpos, ypos, xshift, xv, yshift)
  for (i = 0; i < nmax; i++) {
    double nr, lambda = lbda[i] * 1e-4;
    if (method == MUSE_DAR_METHOD_OWENS) {
      nr = muse_dar_owens_nrindex(lambda, d1, d2);
    } else if (method == MUSE_DAR_METHOD_EDLEN) {
      nr = muse_dar_edlen_nrindex(lambda, t, p, pv);
    } else if (method == MUSE_DAR_METHOD_CIDDOR) {
      nr = muse_dar_ciddor_nrindex(lambda, temp, p, xv, 450.);
    } else {
      nr = muse_dar_filippenko_nrindex(lambda, temp, fp, pres);
    }
    double dr = dr0 * (nr0 - nr);
    /* correct coordinates directly in the pixel table */
    xpos[i] += xshift * dr;
    ypos[i] += yshift * dr;
  } /* for i (all pixel table rows) */
  /* add the header */
  cpl_propertylist_update_double(aPixtable->header, MUSE_HDR_PT_DAR_NAME,
                                 aLambdaRef);
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_DAR_NAME,
                               MUSE_HDR_PT_DAR_COMMENT);
  /* need to recompute the (spatial) pixel table limits, but save the  *
   * previous contents because that's what we need for WCS calibration */
  cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XLO,
                                cpl_propertylist_get_float(aPixtable->header,
                                                           MUSE_HDR_PT_XLO));
  cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XHI,
                                cpl_propertylist_get_float(aPixtable->header,
                                                           MUSE_HDR_PT_XHI));
  cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YLO,
                                cpl_propertylist_get_float(aPixtable->header,
                                                           MUSE_HDR_PT_YLO));
  cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YHI,
                                cpl_propertylist_get_float(aPixtable->header,
                                                           MUSE_HDR_PT_YHI));
  muse_pixtable_compute_limits(aPixtable);

  return CPL_ERROR_NONE;
} /* muse_dar_correct() */

/*----------------------------------------------------------------------------*/
/**
  @brief  check that the continuum of objects in the frame is well-aligned
          perpendicular to the spatial axis, to determine if the correction for
          differential atmospheric refraction (DAR) was sufficient
  @param  aPixtable   the input pixel table to check and correct
  @param  aMaxShift   the output parameter in which the maximum shift of the
                      input data is returned (in arcsec)
  @param  aCorrect    switch to signify that the pixel table should be
                      corrected for the derived residual shifts
  @param  aCube       the optional output cube
  @return CPL_ERROR_NONE on success another CPL error code on failure
  @remark The return value is just a diagnostic flag, the interesting value
          is returned in aMaxShift. If an error occurs, aMaxShift is set to -99.
  @remark This function will add a FITS header (@ref MUSE_HDR_PT_DAR_CHECK) with
          the value of aMaxShift to the pixel table, so that other functions can
          detect, if DAR check has been run before on the same data.
  @remark This function will add a FITS header (@ref MUSE_HDR_PT_DAR_CORR)
          with the value of the reference wavelength to the pixel table, if
          the residual correction was done.

  Resample the input pixel table to a cube with coarse wavelength sampling (for
  reasons of speed and S/N). Find objects on the central plane of the cube and
  follow the center of each object to the first and last plane. The center is
  computed by a marginal centroid first, then refined using a (Gaussian) fit.
  (The centroid and fit are computed in a 11x11 pix box, if the data has
  previously been corrected for DAR effects -- either using this function or
  muse_dar_correct() --, in a 31x31 pix box otherwise.)
  Compute the residual offsets for each object, average the individual results,
  and derive polynomials for the shifts, separately in x- and y-direction, for
  smooth correction with wavelength. Reset the zeropoint of both polynomials to
  be at the reference wavelength. Then use the polynomial to search for the
  extrema and those to finally derive the maximal shift.

  The input pixel table can be corrected for the residual shift. In that case,
  it can be resampled onto a cube again afterwards for debugging.

  @qa If there are significant residual shifts, they are visually apparent in
      any FITS cube viewer by sliding through the cube. The centroid of objects
      in each wavelength bin can also be measured externally to uncover small
      (sub-pixel) systematic shifts that cannot be seen by eye.

  @error{return CPL_ERROR_NULL_INPUT, aPixtable or aMaxShift are NULL}
  @error{return CPL_ERROR_INVALID_TYPE,
         pixel table WCS type is not MUSE_PIXTABLE_WCS_PIXEL}
  @error{output info message\, do it again,
         DAR correction for the input pixel table was already checked}
  @error{output info message\, do it again,
         residual DAR correction for the input pixel table was already applied}
  @error{propagate error of muse_resampling_cube,
         resampling/saving output cube failed}
  @error{return CPL_ERROR_DATA_NOT_FOUND,
         no usable objects found in the datacube}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_dar_check(muse_pixtable *aPixtable, double *aMaxShift,
               cpl_boolean aCorrect, muse_datacube **aCube)
{
  cpl_ensure_code(aMaxShift, CPL_ERROR_NULL_INPUT);
  *aMaxShift = -99.;
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  cpl_ensure_code(wcstype == MUSE_PIXTABLE_WCS_PIXEL, CPL_ERROR_INVALID_TYPE);
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_CHECK)) {
    cpl_msg_info(__func__, "pixel table already checked for DAR accuracy");
  }
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_CORR)) {
    cpl_msg_info(__func__, "pixel table already corrected for DAR residuals");
  }
  /* set the halfsize of the window to measure the centroids with respect to *
   * the reference wavelength; +/-5 pix should suffice in case DAR was       *
   * previously corrected; in other cases, increase it to +/-15 pix          */
  int cenhalfsize = 5;
  /* polynomial order for the fits, default 2, if not corrected yet, use 4 */
  int order = 2;

  /* create cube, with coarse sampling in wavelength */
  muse_resampling_params *params =
    muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
  params->pfx = 1.; /* large pixfrac to not create sudden spatial shifts */
  params->pfy = 1.;
  params->pfl = 1.;
  params->dlambda = 10.;
  params->crsigma = 7.; /* moderate CR rejection */
  muse_pixgrid *grid;
  muse_datacube *cube = muse_resampling_cube(aPixtable, params, &grid);
  muse_resampling_params_delete(params);
  if (!cube) {
    muse_pixgrid_delete(grid);
    muse_pixtable_reset_dq(aPixtable, EURO3D_COSMICRAY);
    return cpl_error_set_message(__func__, cpl_error_get_code(),
                                 "Failure while creating cube!");
  }
  if (aCube) {
    *aCube = cube;
  }
  double crpix3 = cpl_propertylist_get_double(cube->header, "CRPIX3"),
         crval3 = cpl_propertylist_get_double(cube->header, "CRVAL3"),
         cdelt3 = cpl_propertylist_get_double(cube->header, "CD3_3");
  cpl_errorstate state = cpl_errorstate_get(); /* header may be missing */
  double lambdaref = cpl_propertylist_get_double(aPixtable->header,
                                                 MUSE_HDR_PT_DAR_NAME),
         lambda1 = (2 - crpix3) * cdelt3 + crval3, /* 2nd plane */
         lambda3 = (grid->size_z - 1 - crpix3) * cdelt3 + crval3, /* next to last */
         lambda2 = (lambda1 + lambda3) / 2.; /* central plane */
  if (!cpl_errorstate_is_equal(state) || lambdaref < 0) {
    cpl_errorstate_set(state);
    /* use reference wavelength in the center of the wavelength range */
    lambdaref = lambda2;
    cpl_msg_warning(__func__, "Data is not DAR corrected, using reference "
                    "wavelength at %.3f Angstrom!", lambdaref);
    if (!cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_CORR)) {
      cenhalfsize = 15;
      order = 4;
    }
  }
  if (lambdaref > lambda3 || lambdaref < lambda1) { /* ref. outside cube! */
    lambdaref = lambda2;
    cpl_msg_warning(__func__, "Reference lambda outside cube, using reference "
                    "wavelength at %.3f Angstrom!", lambdaref);
  }

  /* detect objects in the cube, using image planes near reference wavelength */
  int iplane, irefplane = lround((lambdaref - crval3) / cdelt3 + crpix3) - 1;
  /* medianing three images removes all cosmic rays but continuum objects stay *
   * (need to use muse_images and the muse_combine function because            *
   * cpl_imagelist_collapse_median_create() disregards all bad pixels)         */
  muse_imagelist *list = muse_imagelist_new();
  unsigned int ilist = 0;
  for (iplane = irefplane - 1; iplane <= irefplane + 1; iplane++) {
    muse_image *image = muse_image_new();
    image->data = cpl_image_duplicate(cpl_imagelist_get(cube->data, iplane));
    image->dq = cpl_image_duplicate(cpl_imagelist_get(cube->dq, iplane));
    image->stat = cpl_image_duplicate(cpl_imagelist_get(cube->stat, iplane));
    muse_imagelist_set(list, image, ilist++);
  } /* for iplane (planes around ref. wavelength) */
  muse_image *median = muse_combine_median_create(list);
  muse_imagelist_delete(list);
  if (!median) {
    if (!aCube) {
      muse_datacube_delete(cube);
    }
    muse_pixgrid_delete(grid);
    muse_pixtable_reset_dq(aPixtable, EURO3D_COSMICRAY);
    return cpl_error_set_message(__func__, cpl_error_get_code(),
                                 "Failure while creating detection image!");
  }
#if DEBUG_MUSE_DARCHECK > 1
  median->header = cpl_propertylist_new(); /* suppress errors while saving */
  cpl_propertylist_append_string(median->header, "BUNIT",
                                 cpl_propertylist_get_string(cube->header,
                                                             "BUNIT"));
  muse_image_save(median, "IMAGE_darcheck_median.fits");
#endif
  /* reject data in image to signify bad pixels to CPL routines */
  muse_image_reject_from_dq(median);
  /* use high sigmas for detection */
  double dsigmas[] = { 50., 30., 10., 8., 6., 5. };
  int ndsigmas = sizeof(dsigmas) / sizeof(double);
  cpl_vector *vsigmas = cpl_vector_wrap(ndsigmas, dsigmas);
  cpl_size isigma = -1;
  state = cpl_errorstate_get();
  cpl_apertures *apertures = cpl_apertures_extract(median->data, vsigmas, &isigma);
  int nx = cpl_image_get_size_x(median->data),
      ny = cpl_image_get_size_y(median->data);
  muse_image_delete(median);
  int napertures = apertures ? cpl_apertures_get_size(apertures) : 0;
  if (napertures < 1) {
    cpl_vector_unwrap(vsigmas);
    cpl_apertures_delete(apertures);
    cpl_errorstate_set(state); /* reset state to before aperture extraction */
    if (!aCube) {
      muse_datacube_delete(cube);
    }
    muse_pixgrid_delete(grid);
    muse_pixtable_reset_dq(aPixtable, EURO3D_COSMICRAY);
    return cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "No sources "
                                 "found for DAR check down to %.1f sigma "
                                 "limit on plane %d", dsigmas[ndsigmas - 1],
                                 irefplane + 1);
  }
  cpl_msg_debug(__func__, "The %.1f sigma threshold was used to find %d source%s,"
                " centered on plane %d", cpl_vector_get(vsigmas, isigma),
                napertures, napertures == 1 ? "" : "s", iplane+1);
  cpl_vector_unwrap(vsigmas);
#if DEBUG_MUSE_DARCHECK
  cpl_apertures_dump(apertures, stdout);
  fflush(stdout);
#if DEBUG_MUSE_DARCHECK > 1
  muse_datacube_save(cube, "DATACUBE_darcheck.fits");
#endif
#endif

  /* restrict box sizes to be at most the size of the image plane */
  int xhalfsize = 2*cenhalfsize > nx ? nx/2 : cenhalfsize,
      yhalfsize = 2*cenhalfsize > ny ? ny/2 : cenhalfsize;

  /* loop through the wavelength planes and compute centroids for all apertures */
  int l, nlambda = cpl_imagelist_get_size(cube->data);
  cpl_vector *vlambda = cpl_vector_new(nlambda);
  cpl_matrix *moffsets = cpl_matrix_new(2, nlambda);
#if !DEBUG_MUSE_DARCHECK
  #pragma omp parallel for default(none)                                       \
          shared(apertures, aPixtable, cdelt3, crpix3, crval3, cube, grid,     \
                 moffsets, napertures, nlambda, nx, ny, vlambda, xhalfsize,    \
                 yhalfsize)
#endif
  for (l = 0; l < nlambda; l++) {
    double xoffset = 0., yoffset = 0.,
           lambda = (l+1 - crpix3) * cdelt3 + crval3;
    int n, noffsets = 0;
    for (n = 1; n <= napertures; n++) {
      /* use detection center to construct (larger) window for centroid */
      double xc = cpl_apertures_get_centroid_x(apertures, n),
             yc = cpl_apertures_get_centroid_y(apertures, n);
      int x1 = lround(xc) - xhalfsize,
          x2 = lround(xc) + xhalfsize,
          y1 = lround(yc) - yhalfsize,
          y2 = lround(yc) + yhalfsize;
      /* force window to be inside the image */
      if (x1 < 1) x1 = 1;
      if (y1 < 1) y1 = 1;
      if (x2 > nx) x2 = nx;
      if (y2 > ny) y2 = ny;

#define MUSE_DARCHECK_MAX_INTERNAL_SHIFT 5 /* accept max. 5 pix shift between *
                                            * image centroid and fit center   */
#if MUSE_DARCHECK_GRID_FITS /* carry out centroid and fit on the un-resampled *
                             * data, i.e on the pixel grid for better S/N     */
#if DEBUG_MUSE_DARCHECK
      const char *method = "grid/moffat";
#endif
      /* transfer data from the pixel table into matrices */
      float *cdata = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
            *cstat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
      int *cdq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);
#define MUSE_DARCHECK_MAX_NPIX 7000 /* start and increase in fixed size bins */
      cpl_matrix *pos = cpl_matrix_new(MUSE_DARCHECK_MAX_NPIX, 2);
      cpl_vector *val = cpl_vector_new(MUSE_DARCHECK_MAX_NPIX),
                 *err = cpl_vector_new(MUSE_DARCHECK_MAX_NPIX);
      int i, npix = 0, nsize = MUSE_DARCHECK_MAX_NPIX;
      for (i = x1 - 1; i < x2; i++) {
        int j;
        for (j = y1 - 1; j < y2; j++) {
          cpl_size idx = muse_pixgrid_get_index(grid, i, j, l, CPL_TRUE),
                   ipx, npx = muse_pixgrid_get_count(grid, idx);
          const cpl_size *rows = muse_pixgrid_get_rows(grid, idx);
          for (ipx = 0; ipx < npx; ipx++) {
            if (cdq[rows[ipx]] != EURO3D_GOODPIXEL) {
              continue;
            }
            if (npix >= nsize) { /* increase size of the buffers */
              nsize += MUSE_DARCHECK_MAX_NPIX;
              cpl_matrix_resize(pos, 0, nsize - cpl_matrix_get_nrow(pos), 0, 0);
              cpl_vector_set_size(val, nsize);
              cpl_vector_set_size(err, nsize);
            }
            cpl_matrix_set(pos, npix, 0, i+1);
            cpl_matrix_set(pos, npix, 1, j+1);
            cpl_vector_set(val, npix, cdata[rows[ipx]]);
            cpl_vector_set(err, npix, sqrt(cstat[rows[ipx]]));
            npix++;
          } /* for ipx */
        } /* for j */
      } /* for i */
      if (npix < 8) { /* need 8 pixels for 8 Moffat parameters */
        cpl_matrix_delete(pos);
        cpl_vector_delete(val);
        cpl_vector_delete(err);
        continue;
      }
      cpl_matrix_set_size(pos, npix, 2);
      cpl_vector_set_size(val, npix);
      cpl_vector_set_size(err, npix);

      /* compute centroid first, as a kind of first guess */
      double xc1, yc1;
      muse_utils_get_centroid(pos, val, NULL/*err*/, &xc1, &yc1,
                              MUSE_UTILS_CENTROID_MEDIAN);

      /* now use a 2D Moffat fit for a refined position */
      cpl_array *pmoffat = cpl_array_new(8, CPL_TYPE_DOUBLE);
      cpl_array_set(pmoffat, 2, xc1);
      cpl_array_set(pmoffat, 3, yc1);
      cpl_error_code rc = muse_utils_fit_moffat_2d(pos, val, err, pmoffat, NULL,
                                                   NULL, NULL, NULL);
      double xc2 = cpl_array_get(pmoffat, 2, NULL),
             yc2 = cpl_array_get(pmoffat, 3, NULL);
      cpl_array_delete(pmoffat);
      cpl_matrix_delete(pos);
      cpl_vector_delete(val);
      cpl_vector_delete(err);

      /* if the fit worked and both methods give roughly similar *
       * positions we should have a valid estimate of the center */
      if (rc == CPL_ERROR_NONE &&
          fabs(xc1 - xc2) < MUSE_DARCHECK_MAX_INTERNAL_SHIFT &&
          fabs(yc1 - yc2) < MUSE_DARCHECK_MAX_INTERNAL_SHIFT) {
        xoffset += xc - xc2;
        yoffset += yc - yc2;
        noffsets++;
      }
#else /* non-MUSE_DARCHECK_GRID_FITS follows, using wavelength plane images */
#if DEBUG_MUSE_DARCHECK
      const char *method = "image/gauss";
#endif
      /* add CPL bad pixel mask in image plane to get reliable results */
      cpl_image *plane = cpl_imagelist_get(cube->data, l),
                *plerr = cpl_image_power_create(cpl_imagelist_get(cube->stat, l), 0.5);
      /* make sure to exclude bad pixels from the fits below */
      muse_quality_image_reject_using_dq(plane, cpl_imagelist_get(cube->dq, l), plerr);
      /* the error image may contain more bad pixels! */
      cpl_image_reject_from_mask(plane, cpl_image_get_bpm(plerr));

      cpl_image *xtr = cpl_image_extract(plane, x1, y1, x2, y2);
      int npix = (x2-x1+1) * (y2-y1+1) /* number of (good) pixels involved */
               - cpl_image_count_rejected(xtr);
      cpl_image_delete(xtr);
      if (npix < 7) { /* need 7 pixels for 7 Gaussian parameters */
        cpl_image_delete(plerr);
        continue;
      }

      /* compute centroid first, as a kind of first guess */
      double xc1, yc1;
      muse_utils_image_get_centroid_window(plane, x1, y1, x2, y2, &xc1, &yc1,
                                           MUSE_UTILS_CENTROID_MEAN);

      /* do Gaussian fit, assuming that the object is roughly stellar */
      cpl_array *pgauss = cpl_array_new(7, CPL_TYPE_DOUBLE);
      cpl_array_set(pgauss, 3, xc1);
      cpl_array_set(pgauss, 4, yc1);
      cpl_error_code rc = cpl_fit_image_gaussian(plane, plerr,
                                                 lround(xc), lround(yc),
                                                 2*xhalfsize, 2*yhalfsize,
                                                 pgauss, NULL, NULL,
                                                 NULL, NULL, NULL,
                                                 NULL, NULL, NULL, NULL);
      double xc2 = cpl_array_get(pgauss, 3, NULL),
             yc2 = cpl_array_get(pgauss, 4, NULL);
      cpl_array_delete(pgauss);
      cpl_image_delete(plerr);

      /* if the fit worked and both methods give roughly similar *
       * positions we should have a valid estimate of the center */
      if (rc == CPL_ERROR_NONE &&
          fabs(xc1 - xc2) < MUSE_DARCHECK_MAX_INTERNAL_SHIFT &&
          fabs(yc1 - yc2) < MUSE_DARCHECK_MAX_INTERNAL_SHIFT) {
        xoffset += xc - xc2;
        yoffset += yc - yc2;
        noffsets++;
      }
#endif /* end of MUSE_DARCHECK_GRID_FITS */
#if DEBUG_MUSE_DARCHECK /* DEBUG output; compare direct centroid and fit position */
      printf("%.1f Angstrom plane %d aper %d (%f,%f): %f %f (%s) %f %f (%d, %s)\n",
             lambda, l+1, n, xc, yc, xc - xc2, yc - yc2, __func__,
             xc2 - xc1, yc2 - yc1, npix, method);
      fflush(stdout);
#endif
    } /* for n (all apertures) */
    /* record average offset in the matrix */
    cpl_vector_set(vlambda, l, lambda);
    cpl_matrix_set(moffsets, 0, l, xoffset / noffsets);
    cpl_matrix_set(moffsets, 1, l, yoffset / noffsets);
#if DEBUG_MUSE_DARCHECK
    printf("%.1f Angstrom plane %d all-apers: %f %f (%s)\n",
           lambda, l+1, xoffset / noffsets, yoffset / noffsets, __func__);
    fflush(stdout);
#endif
  } /* for l (all wavelengths) */
  cpl_apertures_delete(apertures);

  /* compute correction polynomials and maximum relative offset */
  cpl_vector *vlam1 = cpl_vector_duplicate(vlambda),
             *vlam2 = cpl_vector_duplicate(vlambda);
  cpl_matrix *mlam1 = cpl_matrix_wrap(1, nlambda, cpl_vector_unwrap(vlam1)),
             *mlam2 = cpl_matrix_wrap(1, nlambda, cpl_vector_unwrap(vlam2));
  cpl_matrix *mxoff = cpl_matrix_extract_row(moffsets, 0),
             *myoff = cpl_matrix_extract_row(moffsets, 1);
  cpl_vector *vxoff = cpl_vector_wrap(nlambda, cpl_matrix_unwrap(mxoff)),
             *vyoff = cpl_vector_wrap(nlambda, cpl_matrix_unwrap(myoff));
  /* use iterative polynomial fits, separately in both spatial dimensions, *
   * to throw out the most extreme outliers, and compute the correction    */
  double msex, msey, chisqx, chisqy;
  cpl_polynomial *px = muse_utils_iterate_fit_polynomial(mlam1, vxoff,
                                                         NULL, NULL, order, 3.,
                                                         &msex, &chisqx),
                 *py = muse_utils_iterate_fit_polynomial(mlam2, vyoff,
                                                         NULL, NULL, order, 3.,
                                                         &msey, &chisqy);
  cpl_vector_delete(vxoff);
  cpl_vector_delete(vyoff);
  cpl_matrix_delete(mlam1);
  cpl_matrix_delete(mlam2);
#if DEBUG_MUSE_DARCHECK
  printf("polynomial fit in x (order %d, rms=%f, chisq=%g):\n", order,
         sqrt(msex), chisqx);
  cpl_polynomial_dump(px, stdout);
  printf("polynomial fit in y (order %d, rms=%f, chisq=%g):\n", order,
         sqrt(msey), chisqy);
  cpl_polynomial_dump(py, stdout);
  fflush(stdout);
#endif

  /* shift to the zeropoint of the offsets at the reference wavelength; *
   * this should be more stable than just taking the raw input value    */
  double xref = cpl_polynomial_eval_1d(px, lambdaref, NULL),
         yref = cpl_polynomial_eval_1d(py, lambdaref, NULL);
  cpl_size pows = 0; /* set the zero-order coefficient */
  cpl_polynomial_set_coeff(px, &pows, cpl_polynomial_get_coeff(px, &pows) - xref);
  cpl_polynomial_set_coeff(py, &pows, cpl_polynomial_get_coeff(py, &pows) - yref);
#if DEBUG_MUSE_DARCHECK
  printf("polynomial in x (xref=%f at %f --> %f):\n", xref, lambdaref,
         cpl_polynomial_eval_1d(px, lambdaref, NULL));
  cpl_polynomial_dump(px, stdout);
  printf("polynomial in y (yref=%f at %f --> %f):\n", yref, lambdaref,
         cpl_polynomial_eval_1d(py, lambdaref, NULL));
  cpl_polynomial_dump(py, stdout);
  fflush(stdout);
#endif

  /* take the simple approach: search for the largest shift */
  double xmin = DBL_MAX, xmax = -DBL_MAX,
         ymin = DBL_MAX, ymax = -DBL_MAX;
  for (l = 0; l < nlambda; l++) {
    double lambda = cpl_vector_get(vlambda, l),
           xshift = cpl_polynomial_eval_1d(px, lambda, NULL),
           yshift = cpl_polynomial_eval_1d(py, lambda, NULL);
    if (xshift < xmin) xmin = xshift;
    if (xshift > xmax) xmax = xshift;
    if (yshift < ymin) ymin = yshift;
    if (yshift > ymax) ymax = yshift;
#if DEBUG_MUSE_DARCHECK
    printf("%.1f Angstrom plane %d all-fitted: %f %f (%s)\n",
           lambda, l+1, xshift, yshift, __func__);
    fflush(stdout);
#endif
  } /* for l (all wavelengths) */
  cpl_vector_delete(vlambda);
  cpl_matrix_delete(moffsets);

  /* we now have the extrema, compute the max shift per axis */
  double maxdiffx = xmax - xmin,
         maxdiffy = ymax - ymin;
  /* correct from shift in pixels to shift in arcsec */
  if (muse_pfits_get_mode(aPixtable->header) <= MUSE_MODE_WFM_AO_N) {
    maxdiffx *= kMuseSpaxelSizeX_WFM;
    maxdiffy *= kMuseSpaxelSizeY_WFM;
  } else {
    maxdiffx *= kMuseSpaxelSizeX_NFM;
    maxdiffy *= kMuseSpaxelSizeY_NFM;
  }
  *aMaxShift = fmax(maxdiffx, maxdiffy);
  cpl_propertylist_update_double(aPixtable->header, MUSE_HDR_PT_DAR_CHECK,
                                 *aMaxShift);
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_DAR_CHECK,
                               MUSE_HDR_PT_DAR_CHECK_C);

  muse_pixgrid_delete(grid);
  if (!aCube) {
    muse_datacube_delete(cube);
  }

  /* carry out residual DAR correction, if requested */
  if (aCorrect) {
    cpl_msg_debug(__func__, "Correcting pixel table for the shift (max = %f "
                  "arcsec)", *aMaxShift);
    float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
          *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
          *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
    cpl_size i, nrow = muse_pixtable_get_nrow(aPixtable);
    #pragma omp parallel for default(none)                                     \
            shared(lbda, nrow, px, py, xpos, ypos)
    for (i = 0; i < nrow; i++) {
      /* correct coordinates directly in the pixel table */
      xpos[i] += cpl_polynomial_eval_1d(px, lbda[i], NULL);
      ypos[i] += cpl_polynomial_eval_1d(py, lbda[i], NULL);
    } /* for i (all pixel table rows) */

    /* add the header */
    cpl_propertylist_update_double(aPixtable->header, MUSE_HDR_PT_DAR_CORR,
                                   lambdaref);
    cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_DAR_CORR,
                                 MUSE_HDR_PT_DAR_CORR_C);

    /* need to recompute the (spatial) pixel table limits, but save the  *
     * previous contents, if that was not already done                   */
    if (!cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_PREDAR_XLO)) {
      cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XLO,
                                    cpl_propertylist_get_float(aPixtable->header,
                                                               MUSE_HDR_PT_XLO));
      cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XHI,
                                    cpl_propertylist_get_float(aPixtable->header,
                                                               MUSE_HDR_PT_XHI));
      cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YLO,
                                    cpl_propertylist_get_float(aPixtable->header,
                                                               MUSE_HDR_PT_YLO));
      cpl_propertylist_update_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YHI,
                                    cpl_propertylist_get_float(aPixtable->header,
                                                               MUSE_HDR_PT_YHI));
    }
    muse_pixtable_compute_limits(aPixtable);
  } /* if aCorrect */
  cpl_polynomial_delete(px);
  cpl_polynomial_delete(py);
  /* reset cosmic ray statuses in aPixtable, since the CR rejection *
   * done here might not be appropriate for the final datacube      */
  muse_pixtable_reset_dq(aPixtable, EURO3D_COSMICRAY);

  return CPL_ERROR_NONE;
} /* muse_dar_check() */

/**@}*/
