/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2017 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <string.h>

#include "muse_wcs.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_combine.h"
#include "muse_dar.h"
#include "muse_pfits.h"
#include "muse_quality.h"
#include "muse_resampling.h"
#include "muse_utils.h"

#include "muse_kdmatch.h"
/*----------------------------------------------------------------------------*
 *                             Debugging Macros                               *
 *         Set these to 1 or higher for (lots of) debugging output            *
 *----------------------------------------------------------------------------*/
#define FAKE_POS_ROT 0 /* activate some fake positions+rotation for debugging */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_wcs          World coordinate system (WCS) related functions
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @brief   Allocate memory for a new <tt>muse_wcs_object</tt> object.
  @return  a new <tt>muse_wcs_object *</tt> or @c NULL on error
  @remark  The returned object has to be deallocated using
           <tt>muse_wcs_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.

  Simply allocate memory to store the pointers of the <tt>muse_wcs_object</tt>
  structure.
 */
/*----------------------------------------------------------------------------*/
muse_wcs_object *
muse_wcs_object_new(void)
{
  muse_wcs_object *wcs = cpl_calloc(1, sizeof(muse_wcs_object));
  return wcs;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Deallocate memory associated to a muse_wcs_object.
  @param   aWCSObj   input MUSE wcs 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_wcs_object_delete(muse_wcs_object *aWCSObj)
{
  if (!aWCSObj) {
    return;
  }
  muse_datacube_delete(aWCSObj->cube);
  aWCSObj->cube = NULL;
  cpl_table_delete(aWCSObj->detected);
  aWCSObj->detected = NULL;
  cpl_propertylist_delete(aWCSObj->wcs);
  aWCSObj->wcs = NULL;
  cpl_free(aWCSObj);
}

/*---------------------------------------------------------------------------*/
/**
  @brief   Definition of the table structure for the astrometric field detections.

  - <tt>XPOS</tt>: horizontal position
  - <tt>YPOS</tt>: vertical position
  - <tt>XERR</tt>: error estimate of the horizontal position
  - <tt>YERR</tt>: error estimate of the vertical position
  - <tt>FLUX</tt>: (relative) flux of the source
  - <tt>XFWHM</tt>: horizontal FWHM
  - <tt>YFWHM</tt>: vertical FWHM
 */
/*---------------------------------------------------------------------------*/
const muse_cpltable_def muse_wcs_detections_def[] = {
  { "XPOS", CPL_TYPE_DOUBLE, "pix", "%f", "horizontal position", CPL_TRUE },
  { "YPOS", CPL_TYPE_DOUBLE, "pix", "%f", "vertical position", CPL_TRUE },
  { "XERR", CPL_TYPE_DOUBLE, "pix", "%f",
    "error estimate of the horizontal position", CPL_TRUE },
  { "YERR", CPL_TYPE_DOUBLE, "pix", "%f",
    "error estimate of the vertical position", CPL_TRUE },
  { "FLUX", CPL_TYPE_DOUBLE, "count", "%e", "(relative) flux of the source", CPL_TRUE },
  { "XFWHM", CPL_TYPE_DOUBLE, "pix", "%f", "horizontal FWHM", CPL_TRUE },
  { "YFWHM", CPL_TYPE_DOUBLE, "pix", "%f", "vertical FWHM", CPL_TRUE },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*---------------------------------------------------------------------------*/
/**
  @brief   Definition of the table structure for the astrometric reference
           sources.

  - <tt>SourceID</tt>: the source identification
  - <tt>RA</tt>: right ascension (in decimal degrees)
  - <tt>DEC</tt>: declination (in decimal degrees)
  - <tt>filter</tt>: the filter used for the magnitude
  - <tt>mag</tt>: the object (Vega) magnitude
 */
/*---------------------------------------------------------------------------*/
const muse_cpltable_def muse_wcs_reference_def[] = {
  { "SourceID", CPL_TYPE_STRING, "", "%s", "the source identification", CPL_TRUE },
  { "RA", CPL_TYPE_DOUBLE, "deg", "%f", "right ascension (decimal degrees)", CPL_TRUE },
  { "DEC", CPL_TYPE_DOUBLE, "deg", "%f", "declination (decimal degrees)", CPL_TRUE },
  { "filter", CPL_TYPE_STRING, "", "%s", "the filter used for the magnitude", CPL_TRUE },
  { "mag", CPL_TYPE_DOUBLE, "mag", "%f", "the object (Vega) magnitude", CPL_TRUE },
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};


/*----------------------------------------------------------------------------*/
/**
  @brief   Detect and centroid stars on an image of an astrometric exposure.
  @param   aImage      reconstructed image of the astrometric exposure
  @param   aSigma      the sigma level used for object detection
  @param   aCentroid   type of centroiding algorithm to use
  @return  the table with object properties on success or NULL on failure
  @remark  aCentroid be either MUSE_WCS_CENTROID_GAUSSIAN,
           MUSE_WCS_CENTROID_MOFFAT, or MUSE_WCS_CENTROID_BOX.

  Find objects in this image and determine the centroid of each object by
  fitting a two-dimensional function of the given type to it. Record the
  resulting central pixel coordinates, an estimate of their positional error,
  and their fluxes and FWHMs in the output table.  Exclude objects where FWHM
  estimates cannot be determined from the image data, since they likely lie on
  the edge of the image.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImage or its data or stat components are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL, aSigma is not positive}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         an unknown centroiding type was requested in aCentroid}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         no sources were detected at the given sigma level}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_wcs_centroid_stars(muse_image *aImage, float aSigma,
                        muse_wcs_centroid_type aCentroid)
{
  cpl_ensure(aImage && aImage->data && aImage->stat, CPL_ERROR_NULL_INPUT,
             NULL);
  cpl_ensure(aSigma > 0., CPL_ERROR_ILLEGAL_INPUT, NULL);
  switch (aCentroid) {
  case MUSE_WCS_CENTROID_GAUSSIAN:
    cpl_msg_info(__func__, "Gaussian profile fits for object centroiding");
    break;
  case MUSE_WCS_CENTROID_MOFFAT:
    cpl_msg_info(__func__, "Moffat profile fits for object centroiding");
    break;
  case MUSE_WCS_CENTROID_BOX:
    cpl_msg_info(__func__, "Simple square box object centroiding");
    break;
  default:
    cpl_msg_error(__func__, "Unknown centroiding method!");
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    return NULL;
  }

  /* muse_image_duplicate() doesn't work here, because we don't want    *
   * to require the dq component, so do it "by hand"; then we can       *
   * convert the variance to an error = sqrt(variance) at the same time */
  muse_image *image = muse_image_new();
  image->data = cpl_image_duplicate(aImage->data);
  image->stat = cpl_image_power_create(aImage->stat, 0.5);
  if (aImage->header) {
    image->header = cpl_propertylist_duplicate(aImage->header);
  }
  /* make sure to exclude bad pixels from the fits below,  *
   * either from an existing dq component or by using NANs */
  if (aImage->dq) {
    image->dq = cpl_image_duplicate(aImage->dq);
    muse_quality_image_reject_using_dq(image->data, image->dq, image->stat);
  } else {
    cpl_image_reject_value(image->data, CPL_VALUE_NAN); /* mark NANs */
    cpl_image_reject_value(image->stat, CPL_VALUE_NAN);
  }
  /* since cpl_image_get_fwhm() ignores the image masks,  *
   * treat the masked areas so that they don't contribute */
  cpl_image_fill_rejected(image->data, cpl_image_get_min(image->data));
  cpl_image_fill_rejected(image->stat, cpl_image_get_max(image->stat));

  /* do the source detection */
  cpl_apertures *apertures = cpl_apertures_extract_sigma(image->data, aSigma);
  int ndet = apertures ? cpl_apertures_get_size(apertures) : 0;
  if (ndet < 1) {
    muse_image_delete(image);
    cpl_msg_error(__func__, "No sources for astrometric calibration found down "
                  "to %.3f sigma limit", aSigma);
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    return NULL;
  }
  double med_dist, median = cpl_image_get_median_dev(image->data, &med_dist),
         threshold = median + aSigma * med_dist;
  cpl_msg_debug(__func__, "The %.3f sigma threshold (%.3f, %.3f +/- %.3f) was "
                "used to find %d sources", aSigma, threshold, median, med_dist,
                ndet);

  int nx = cpl_image_get_size_x(image->data),
      ny = cpl_image_get_size_y(image->data);
  /* parameters for the fits */
  const double bgguess = cpl_image_get_median(image->data),
               beta = 2.5, /* moffat beta */
               moffat_alpha_to_fwhm = 1 / (2 * sqrt(pow(2, 1/beta) - 1));
#if 0 /* unused idea to further robustify FWHM estimate */
  double fwhmguess = (muse_pfits_get_fwhm_start(image->header)
                      + muse_pfits_get_fwhm_end(image->header)) / 2.;
  if (fwhmguess < FLT_EPSILON) { /* in case FWHM headers are not there */
    fwhmguess = 4.; /* [pix] assume median seeing in WFM */
  } else { /* use nominal MUSE spaxel scale to get FWHM guess [pix] */
    if (muse_pfits_get_mode(image->header) < MUSE_MODE_NFM_AO_N) {
      fwhmguess /= (kMuseSpaxelSizeX_WFM + kMuseSpaxelSizeY_WFM) / 2;
    } else {
      fwhmguess /= (kMuseSpaxelSizeX_NFM + kMuseSpaxelSizeY_NFM) / 2;
    }
  }
#endif

  /* create the output table and loop over all apertures *
   * to centroid the detected stars and fill the table   */
  cpl_table *detections = muse_cpltable_new(muse_wcs_detections_def, ndet);
  cpl_table_unselect_all(detections);
  int idet;
  for (idet = 0; idet < ndet; idet++) {
    double xc = cpl_apertures_get_centroid_x(apertures, idet+1),
           yc = cpl_apertures_get_centroid_y(apertures, idet+1),
           fluxaper = cpl_apertures_get_flux(apertures, idet+1);
    double xwaper, ywaper;
    cpl_image_get_fwhm(image->data, lround(xc), lround(yc), &xwaper, &ywaper);
    /* Remove the detection if the FWHM could not be computed in both    *
     * axes, since this likely points to an object on the border or with *
     * some other problem which should not be relied on for centroiding. */
    if (xwaper < 0 || ywaper < 0 || xwaper > 100. || ywaper > 100.) {
      cpl_msg_debug(__func__, "FWHM computation unsuccessful at %f,%f, result "
                    "was %.3f,%.3f", xc, yc, xwaper, ywaper);
      cpl_table_select_row(detections, idet);
      continue;
    }

    /* set the halfsize of the window to measure the centroids *
     * with respect to the detection; +/-5 pix should suffice  */
    int x1 = floor(xc) - 5,
        x2 = ceil(xc) + 5,
        y1 = floor(yc) - 5,
        y2 = ceil(yc) + 5;
    /* 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;

    double xcen, ycen, xerr = 0., yerr = 0., xw, yw, flux;
    cpl_errorstate state = cpl_errorstate_get();
    switch (aCentroid) {
    case MUSE_WCS_CENTROID_GAUSSIAN: {
      cpl_array *pgauss = cpl_array_new(7, CPL_TYPE_DOUBLE),
                *pgerr = cpl_array_new(7, CPL_TYPE_DOUBLE),
                *pgfit = cpl_array_new(7, CPL_TYPE_INT);
      /* first, only fit the centroid */
      cpl_array_set(pgfit, 3, 1); /* xc */
      cpl_array_set(pgfit, 4, 1); /* yc */
      cpl_array_set(pgfit, 0, 0);
      cpl_array_set(pgfit, 1, 0);
      cpl_array_set(pgfit, 2, 0);
      cpl_array_set(pgfit, 5, 0);
      cpl_array_set(pgfit, 6, 0);
      cpl_array_set(pgauss, 3, xc);
      cpl_array_set(pgauss, 4, yc);
      cpl_array_set(pgauss, 0, bgguess);
      cpl_array_set(pgauss, 1, fluxaper);
      cpl_array_set(pgauss, 2, 0.); /* rho */
      cpl_array_set(pgauss, 5, xwaper * CPL_MATH_SIG_FWHM);
      cpl_array_set(pgauss, 6, ywaper * CPL_MATH_SIG_FWHM);
      cpl_fit_image_gaussian(image->data, image->stat, lround(xc), lround(yc),
                             x2-x1+1, y2-y1+1, pgauss, pgerr, pgfit, NULL, NULL,
                             NULL, NULL, NULL, NULL, NULL);
      xcen = cpl_array_get(pgauss, 3, NULL);
      ycen = cpl_array_get(pgauss, 4, NULL);
      xerr = cpl_array_get(pgerr, 3, NULL);
      yerr = cpl_array_get(pgerr, 4, NULL);
      /* second, keep center fixed, only fit flux and width */
      cpl_array_set(pgfit, 3, 0); /* xc */
      cpl_array_set(pgfit, 4, 0); /* yc */
      cpl_array_set(pgfit, 1, 1); /* flux */
      cpl_array_set(pgfit, 5, 1); /* sigma_x */
      cpl_array_set(pgfit, 6, 1); /* sigma_y */
      cpl_fit_image_gaussian(image->data, image->stat, lround(xc), lround(yc),
                             x2-x1+1, y2-y1+1, pgauss, pgerr, pgfit, NULL, NULL,
                             NULL, NULL, NULL, NULL, NULL);
      xw = cpl_array_get(pgauss, 5, NULL) * CPL_MATH_FWHM_SIG;
      yw = cpl_array_get(pgauss, 6, NULL) * CPL_MATH_FWHM_SIG;
      flux = cpl_array_get(pgauss, 1, NULL);
      cpl_array_delete(pgauss);
      cpl_array_delete(pgerr);
      cpl_array_delete(pgfit);
      break;
      }
    case MUSE_WCS_CENTROID_MOFFAT: {
      cpl_size nmax = (x2-x1+1) * (y2-y1+1);
      cpl_matrix *pos = cpl_matrix_new(nmax, 2);
      cpl_vector *val = cpl_vector_new(nmax),
                 *err = cpl_vector_new(nmax);
      int i, npix = 0;
      for (i = x1; i <= x2; i++) {
        int j;
        for (j = y1; j <= y2; j++) {
          int error;
          double value = cpl_image_get(image->data, i, j, &error);
          if (error != 0) {
            continue;
          }
          cpl_matrix_set(pos, npix, 0, i);
          cpl_matrix_set(pos, npix, 1, j);
          cpl_vector_set(val, npix, value);
          /* stat is already sigma! */
          cpl_vector_set(err, npix, cpl_image_get(image->stat, i, j, &error));
          npix++;
        } /* for j */
      } /* for i */
      cpl_matrix_set_size(pos, npix, 2);
      cpl_vector_set_size(val, npix);
      cpl_vector_set_size(err, npix);
      cpl_array *pmoffat = cpl_array_new(8, CPL_TYPE_DOUBLE),
                *pmerror = cpl_array_new(8, CPL_TYPE_DOUBLE),
                *pmfit = cpl_array_new(8, CPL_TYPE_INT);
      /* first, only fit the centroid */
      cpl_array_set(pmfit, 2, 1); /* xc */
      cpl_array_set(pmfit, 3, 1); /* yc */
      cpl_array_set(pmfit, 0, 0);
      cpl_array_set(pmfit, 1, 0);
      cpl_array_set(pmfit, 4, 0);
      cpl_array_set(pmfit, 5, 0);
      cpl_array_set(pmfit, 6, 0);
      cpl_array_set(pmfit, 7, 0);
      cpl_array_set(pmoffat, 2, xc);
      cpl_array_set(pmoffat, 3, yc);
      cpl_array_set(pmoffat, 0, bgguess);
      cpl_array_set(pmoffat, 1, fluxaper);
      cpl_array_set(pmoffat, 4, xwaper * moffat_alpha_to_fwhm);
      cpl_array_set(pmoffat, 5, ywaper * moffat_alpha_to_fwhm);
      cpl_array_set(pmoffat, 6, beta);
      cpl_array_set(pmoffat, 7, 0.); /* rho */
      muse_utils_fit_moffat_2d(pos, val, err, pmoffat, pmerror, pmfit, NULL, NULL);
      xcen = cpl_array_get(pmoffat, 2, NULL);
      ycen = cpl_array_get(pmoffat, 3, NULL);
      xerr = cpl_array_get(pmerror, 2, NULL);
      yerr = cpl_array_get(pmerror, 3, NULL);
      /* second, keep center fixed, only fit flux and width */
      cpl_array_set(pmfit, 2, 0); /* xc */
      cpl_array_set(pmfit, 3, 0); /* yc */
      cpl_array_set(pmfit, 1, 1); /* flux */
      cpl_array_set(pmfit, 4, 1); /* xhwhm */
      cpl_array_set(pmfit, 5, 1); /* yhwhm */
      muse_utils_fit_moffat_2d(pos, val, err, pmoffat, pmerror, pmfit, NULL, NULL);
      xw = cpl_array_get(pmoffat, 4, NULL) / moffat_alpha_to_fwhm;
      yw = cpl_array_get(pmoffat, 5, NULL) / moffat_alpha_to_fwhm;
      flux = cpl_array_get(pmoffat, 1, NULL);
      cpl_array_delete(pmoffat);
      cpl_array_delete(pmerror);
      cpl_array_delete(pmfit);
      cpl_matrix_delete(pos);
      cpl_vector_delete(val);
      cpl_vector_delete(err);
      break;
      }
    default: /* MUSE_WCS_CENTROID_BOX is left */
      muse_utils_image_get_centroid_window(image->data, x1, y1, x2, y2,
                                           &xcen, &ycen,
                                           MUSE_UTILS_CENTROID_MEDIAN);
#if 0
      printf("%d apertures %f %f boxes %f %f deltas %f %f\n", idet+1, xc, yc,
             xcen, ycen, xcen - xc, ycen - yc);
      fflush(stdout);
#endif
      /* set error to 0.15 pix which is the typical *
       * stdev compared to Gaussian fits            */
      xerr = 0.15;
      yerr = 0.15;
      /* compute FWHM again with the revised central position */
      cpl_image_get_fwhm(image->data, lround(xcen), lround(ycen), &xw, &yw);
      flux = fluxaper; /* take the aperture flux */
    }

    /* mandatory columns: position */
    cpl_table_set(detections, "XPOS", idet, xcen);
    cpl_table_set(detections, "YPOS", idet, ycen);
    /* and the error on the position */
    cpl_table_set(detections, "XERR", idet, xerr);
    cpl_table_set(detections, "YERR", idet, yerr);
    /* extra columns: FWHM... */
    cpl_table_set(detections, "XFWHM", idet, xw);
    cpl_table_set(detections, "YFWHM", idet, yw);
    /* ... and flux */
    cpl_table_set(detections, "FLUX", idet, flux);

    if (cpl_errorstate_is_equal(state) && xw > 0 && yw > 0 &&
        xcen >= 1 && xcen <= nx && ycen >= 1 && ycen <= ny &&
        xw < 100. && yw < 100. && xerr < 100. && yerr < 100.) {
      continue;
    }
    /* some error occurred, so mark this entry for removal */
    cpl_table_select_row(detections, idet);
  } /* for idet (aperture index) */
  cpl_table_erase_selected(detections);
  cpl_apertures_delete(apertures);
  muse_image_delete(image);
  cpl_msg_debug(__func__, "%d of %d sources were recorded in the detections "
                "table", (int)cpl_table_get_nrow(detections), ndet);

  return detections;
} /* muse_wcs_centroid_stars() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Determine the centers of stars on an astrometric exposure.
  @param   aPixtable   the input pixel table containing the astrometric exposure
  @param   aSigma      the sigma level used for object detection
  @param   aCentroid   type of centroiding algorithm to use
  @param   aWCSObj     the MUSE WCS object
  @return  CPL_ERROR_NONE on success, another CPL error code on failure
  @remark  The table returned in aWCSObj->detected contains the x and y
           positions of all detected sources. This function also stores the
           center (components xcenter, ycenter and ra, dec) of the dataset in
           aWCSObj as well as the datacube used for the detection (component
           cube).
  @remark  This function assumes that the differential atmospheric
           refraction was already corrected in the data beforehand, this will
           be checked by searching for the respective FITS headers
           (@ref MUSE_HDR_PT_DAR_NAME or @ref MUSE_HDR_PT_DAR_CORR). If this is
           not the case, a warning is printed and an error is set, but the data
           is fully processed and the return code is not changed.
  @remark  aCentroid be either MUSE_WCS_CENTROID_GAUSSIAN,
           MUSE_WCS_CENTROID_MOFFAT, or MUSE_WCS_CENTROID_BOX.

  Resample the input pixel table to a datacube with very coarse wavelength
  sampling (50 Angstrom/pixel) and use the central three wavelength planes to
  reconstruct a median-combined image. This special image is also appended to
  the list of reconstructed image in the created datacube, under the (extension)
  name @ref MUSE_WCS_DETIMAGE_EXTNAME, as 2nd image after the white-light image.
  The special detection image is the passed on to @ref muse_wcs_centroid_stars()
  to compute the exact centroids using the given method aCentroid in a table.

  @error{return CPL_ERROR_NULL_INPUT,
         inputs aPixtable\, its header component\, or aWCSObj are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aSigma was non-positive}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         an unknown centroiding type was requested}
  @error{output warning\, set CPL_ERROR_UNSUPPORTED_MODE\, but continue processing,
         input WFM pixel table was not corrected for DAR}
  @error{propagate error of muse_resampling_cube(), cube could not be created}
  @error{propagate error, image could not be created from cube}
  @error{propagate error from muse_wcs_centroid_stars(),
         sources detection/centroiding failed}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_locate_sources(muse_pixtable *aPixtable, float aSigma,
                        muse_wcs_centroid_type aCentroid,
                        muse_wcs_object *aWCSObj)
{
  cpl_ensure_code(aPixtable && aPixtable->header && aWCSObj,
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aSigma > 0., CPL_ERROR_ILLEGAL_INPUT);
  switch (aCentroid) {
  case MUSE_WCS_CENTROID_GAUSSIAN:
  case MUSE_WCS_CENTROID_MOFFAT:
  case MUSE_WCS_CENTROID_BOX:
    break;
  default:
    return cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
  }
  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) > 2) {
    const char *fn = "wcs__pixtable.fits";
    cpl_msg_info(__func__, "Saving pixel table as \"%s\"", fn);
    muse_pixtable_save(aPixtable, fn);
  }

  /* check that DAR correction was carried out in some way */
  cpl_boolean darcorrected = CPL_FALSE;
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_NAME) &&
      cpl_propertylist_get_double(aPixtable->header, MUSE_HDR_PT_DAR_NAME) > 0.) {
    darcorrected = CPL_TRUE;
  } else if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_DAR_CORR)) {
    darcorrected = CPL_TRUE;
  }
  cpl_boolean isNFM = muse_pfits_get_mode(aPixtable->header) == MUSE_MODE_NFM_AO_N;
  if (!darcorrected && !isNFM) {
    const char *message = "Correction for differential atmospheric refraction "
                          "was not applied! Deriving astrometric correction "
                          "from such data is unlikely to give good results!";
    cpl_msg_warning(__func__, "%s", message);
    cpl_error_set_message(__func__, CPL_ERROR_UNSUPPORTED_MODE, "%s", message);
  }

  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.;
  params->dlambda = 50.; /* we can integrate over lots of Angstrom here */
  params->crtype = MUSE_RESAMPLING_CRSTATS_MEDIAN; /* median for clean cube */
  params->crsigma = 25.; /* very moderate CR rejection */
  muse_datacube *cube = muse_resampling_cube(aPixtable, params, NULL);
  muse_resampling_params_delete(params);
  /* 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);
  if (!cube) {
    return cpl_error_set_message(__func__, cpl_error_get_code(),
                                 "Failure while creating cube!");
  }
  aWCSObj->cube = cube;
  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) >= 2) {
    const char *fn = "wcs__cube.fits";
    cpl_msg_info(__func__, "Saving cube as \"%s\"", fn);
    muse_datacube_save(cube, fn);
  }

  /* detect objects in the cube, using image planes around the central one */
  int iplane, irefplane = cpl_imagelist_get_size(cube->data) / 2;
  /* 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);
  /* fill existing empty header with the header of the *
   * cube, but without the third dimension in the WCS  */
  cpl_propertylist_copy_property_regexp(median->header, cube->header,
                                        "^C...*3$|^CD3_.$|^SPECSYS$", 1);
  muse_imagelist_delete(list);
  if (!median) {
    return cpl_error_set_message(__func__, cpl_error_get_code(),
                                 "Failure while creating detection image!");
  }
  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) >= 2) {
    const char *fn = "wcs__image_median.fits";
    cpl_msg_info(__func__, "Saving median detection image as \"%s\"", fn);
    muse_image_save(median, fn);
  }
  cube->recimages = muse_imagelist_new();
  cube->recnames = cpl_array_new(2, CPL_TYPE_STRING);
  /* create a white-light image; this is unused here but should *
   * be saved as first extension in the output cube             */
  muse_table *fwhite = muse_table_load_filter(NULL, "white");
  muse_image *white = muse_datacube_collapse(cube, fwhite);
  muse_table_delete(fwhite);
  muse_imagelist_set(cube->recimages, white, 0);
  cpl_array_set_string(cube->recnames, 0, "white");
  /* append this median-combined detection image also as *
   * part of the reconstructed images of the datacube    */
  muse_imagelist_set(cube->recimages, median, 1);
  cpl_array_set_string(cube->recnames, 1, MUSE_WCS_DETIMAGE_EXTNAME);

  /* now do the centroiding of all detected sources */
  cpl_table *detections = muse_wcs_centroid_stars(median, aSigma, aCentroid);
  if (!detections || (detections && cpl_table_get_nrow(detections) < 1)) {
    return cpl_error_get_code();
  }
  /* save pixel value and sky coordinates of center of the data */
  aWCSObj->xcenter = cpl_image_get_size_x(median->data) / 2.,
  aWCSObj->ycenter = cpl_image_get_size_y(median->data) / 2.;
  aWCSObj->ra =  muse_pfits_get_ra(median->header);
  aWCSObj->dec = muse_pfits_get_dec(median->header);
#if 0
  cpl_msg_debug(__func__, "image size: %d x %d --> center %f, %f",
                (int)cpl_image_get_size_x(median->data),
                (int)cpl_image_get_size_y(median->data),
                aWCSObj->xcenter, aWCSObj->ycenter);
#endif

  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) >= 2) {
    const char *fn = "wcs__detections.fits";
    cpl_msg_info(__func__, "Saving table of detections as \"%s\"", fn);
    cpl_table_save(detections, NULL, NULL, fn, CPL_IO_CREATE);
  }

  aWCSObj->detected = detections;
  return CPL_ERROR_NONE;
} /* muse_wcs_locate_sources() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Find the world coordinate solution for a given field using
           coordinates of detected sources and coordinates of reference values.
  @param   aWCSObj      the MUSE WCS object
  @param   aReference   reference coordinates for the same field
  @param   aRadius      initial radius for pattern matching in pixels
  @param   aFAccuracy   If a positive number, factor to set initial accuracy
                        relative to mean position accuracy in
                        aWCSObj->detected in the "triangle" pattern matching
                        algorith. A value of zero switches to the kdtree based
                        quadruple pattern matching algorithm.
  @param   aIter        number of iterations
  @param   aThresh      the rejection threshold
  @return  CPL_ERROR_NONE on success, another CPL error code on failure
  @remark  The propertylist returned in aWCSObj->wcs contains the world
           coordinate solution.
  @remark  Both input object tables (aWCSObj->detected and aReference) are
           modified in that they are sorted by increasing source flux.

  Identify reference objects for the detected objects using pattern matching.

  There are two pattern matching algorithm implemented, which can be
  selected by chosing a positive or zero/negative value of aFAccuracy.

  In the first method (with a positive value of aFAccuracy), start
  using a search radius of aRadius pixels, and iteratively decrease
  it, until no duplicate detections are identified any
  more. Similarly, iterate the data accuracy (decrease it downwards
  from the mean positioning error aFAccuracy) until matches are
  found. Remove the remaining unidentified objects.

  The second method (when aFAccuracy is set to zero), iterates through
  all quadruples in both the detected objects and the catalogue,
  calculates the transformation and checks whether more than 80% of
  the detections match a catalog entry within aRadius.

  The final values used for relative and absolute accuracy and the
  radius are recorded in the header keywords MUSE_HDR_WCS_RADIUS,
  MUSE_HDR_WCS_ACCURACY (not when using quadruples), and
  MUSE_HDR_WCS_FACCURACY (not when using quadruples).

  The sky coordinates that were associated to each detection are recorded in
  the (new) columns RA and DEC of the table of detected objects
  (aWCSObj->detected). If no reference position could be identified for a given
  detection, the RA and DEC entries will remain invalid.

  Derive the world coordinate plate solution using cpl_wcs_platesol() (taking
  into account shift, 2 scales, and rotation) and add it as cpl_propertylist
  element wcs to aWCSObj.

  @error{return CPL_ERROR_NULL_INPUT, input WCS object is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         table of detected or reference coordinates are empty or NULL}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         table of detected or reference coordinates don't contain the expected columns}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         aRadius and/or aFAccuracy are non-positive}
  @error{return CPL_ERROR_DATA_NOT_FOUND, no objects could be identified}
  @error{propagate return code, cpl_wcs_platesol failed}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_solve(muse_wcs_object *aWCSObj, cpl_table *aReference,
               float aRadius, float aFAccuracy, int aIter, float aThresh)
{
  cpl_ensure_code(aWCSObj, CPL_ERROR_NULL_INPUT);
  cpl_table *detected = aWCSObj->detected;
  int ndet = cpl_table_get_nrow(detected),
      nref = cpl_table_get_nrow(aReference);
  cpl_boolean use_triangle_ppm = (aFAccuracy > 0);
  cpl_ensure_code(ndet > 0 && nref > 0, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(muse_cpltable_check(detected, muse_wcs_detections_def)
                  == CPL_ERROR_NONE &&
                  muse_cpltable_check(aReference, muse_wcs_reference_def)
                  == CPL_ERROR_NONE,
                  CPL_ERROR_BAD_FILE_FORMAT);
  cpl_ensure_code(aRadius > 0. && aFAccuracy >= 0., CPL_ERROR_ILLEGAL_INPUT);

  /* sort tables by the source brightness */
  cpl_propertylist *order = cpl_propertylist_new();
  cpl_propertylist_append_bool(order, "FLUX", TRUE);
  cpl_table_sort(detected, order);
  cpl_propertylist_erase(order, "FLUX");
  cpl_propertylist_append_bool(order, "mag", FALSE);
  cpl_table_sort(aReference, order);
  cpl_propertylist_delete(order);
  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) >= 2) {
    FILE *fp = fopen("wcs__detections.ascii", "w");
    fprintf(fp, "%s: table of detected sources (sorted by flux):\n", __func__);
    cpl_table_dump(detected, 0, 1000, fp);
    fclose(fp);
    fp = fopen("wcs__references.ascii", "w");
    fprintf(fp, "%s: table of reference objects (sorted by flux):\n", __func__);
    cpl_table_dump(aReference, 0, 1000, fp);
    fclose(fp);
  }

  /* construct a basic input WCS */
  cpl_propertylist *wcsin = muse_wcs_create_default(aWCSObj->cube
                                                    ? aWCSObj->cube->header
                                                    : NULL);
  cpl_propertylist_append_double(wcsin, "CRVAL1", aWCSObj->ra);
  cpl_propertylist_append_double(wcsin, "CRVAL2", aWCSObj->dec);
  cpl_propertylist_update_double(wcsin, "CRPIX1", aWCSObj->crpix1);
  cpl_propertylist_update_double(wcsin, "CRPIX2", aWCSObj->crpix2);
  /* add NAXIS to fool cpl_wcs_new_from_propertylist() */
  cpl_propertylist_append_int(wcsin, "NAXIS", 2);
  cpl_propertylist_append_int(wcsin, "NAXIS1", aWCSObj->xcenter * 2.);
  cpl_propertylist_append_int(wcsin, "NAXIS2", aWCSObj->ycenter * 2.);

  /* convert input tables into matrices for the pattern-matching function */
  cpl_matrix *data = cpl_matrix_new(2, ndet),
             *patt = cpl_matrix_new(2, nref);
  int i;
  for (i = 0; i < ndet; i++) {
    cpl_matrix_set(data, 0, i, cpl_table_get(detected, "XPOS", i, NULL));
    cpl_matrix_set(data, 1, i, cpl_table_get(detected, "YPOS", i, NULL));
  } /* for i (all detections) */

  /* use the basic WCS to transform input reference positions *
   * to pixel positions, to take out deformations by gnomonic *
   * projection before attempting pattern matching            */
  for (i = 0; i < nref; i++) {
    double ra = cpl_table_get(aReference, "RA", i, NULL),
           dec = cpl_table_get(aReference, "DEC", i, NULL),
           x, y;
    muse_wcs_pixel_from_celestial(wcsin, ra, dec, &x, &y);
    cpl_matrix_set(patt, 0, i, x);
    cpl_matrix_set(patt, 1, i, y);
#if 0
    printf("conversion: %2d\t%.7f %.7f\t%6.2f %6.2f\n", i + 1, ra, dec, x, y);
    fflush(stdout);
#endif
  } /* for i (all reference points) */
#if 0
  if (cpl_msg_get_level() == CPL_MSG_DEBUG) {
    printf("%s: data matrix:\n", __func__);
    cpl_matrix_dump(data, stdout);
    printf("%s: patt matrix:\n", __func__);
    cpl_matrix_dump(patt, stdout);
    fflush(stdout);
  }
#endif

  /* compute typical data error from the error columns, *
   * to use this as input for the pattern matching      */
  double accuracy0 = sqrt(pow(cpl_table_get_column_mean(detected, "XERR"), 2) +
                          pow(cpl_table_get_column_mean(detected, "YERR"), 2));
  /* start with somewhat worse accuracy to make  *
   * it more likely that a match is found at all */
  double accuracy = accuracy0 * aFAccuracy,
         radius = aRadius; /* start with (hopefully) large search radius */
  int nid = INT_MAX; /* number of identified detections */
  cpl_array *aid = NULL;
  double xscale, yscale;
  muse_wcs_get_scales(wcsin, &xscale, &yscale);
  if (use_triangle_ppm) {
    cpl_boolean dupes = CPL_FALSE;
    do {
      double scale, angle;
      /* As recommended in the CPL docs, initially select the 20 first (brightest!) *
       * detections and the 10 first (brightest) reference sources, so that         *
       * hopefully all reference sources are contained in the brightest detections. */
#define USE_DATA 15
#define USE_PATT 10
      int ndata = ndet < USE_DATA ? ndet : USE_DATA,
        npatt = nref < USE_PATT ? nref : USE_PATT;
      cpl_array_delete(aid);
      do {
        cpl_msg_debug(__func__, "trying pattern matching with accuracy %g and "
                      "radius %g", accuracy, radius);
        aid = cpl_ppm_match_points(data, ndata, accuracy,
                                   patt, npatt, 1. /* [pix] */,
                                   0.1 /* 10% */, radius, NULL, NULL,
                                   &scale, &angle);
        /* decrease accuracy in case pattern matching didn't succeed */
        accuracy /= aid ? 1. : 1.5;
        if (accuracy < FLT_EPSILON) {
          break; /* doesn't make sense to go any lower */
        }
      } while (!aid); /* no matched points likely means to low accuracy */
      cpl_errorstate state = cpl_errorstate_get();
      nid = cpl_array_get_size(aid);
      if (nid > 0) { /* subtract valid only if the array exists */
        nid -= cpl_array_count_invalid(aid);
      }
#if 0
      printf("aid (valid=%d):\n", nid);
      cpl_array_dump(aid, 0, cpl_array_get_size(aid), stdout);
      fflush(stdout);
#endif
      if (nid < 1) {
        cpl_array_delete(aid);
        cpl_matrix_delete(data);
        cpl_matrix_delete(patt);
        cpl_errorstate_set(state); /* swallow error about NULL cpl_array */
        cpl_propertylist_delete(wcsin);
        return cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "None of "
                                     "the %d detections could be identified with "
                                     "the %d reference positions with radius %.3f "
                                     "pix (starting value %.3f) and data accuracy "
                                     "%.3e pix (intrinsic mean error %.3e)", ndet,
                                     nref, radius, aRadius, accuracy, accuracy0);
      }
      /* the first-guess scale computed here only makes sense    *
       * if multiplied by the pixel scale in the first-guess WCS */
      dupes = muse_cplarray_has_duplicate(aid);
      cpl_msg_debug(__func__, "%d %sidentified points [scale = %g, angle = %g; "
                    "used radius = %.3f pix (starting value %.3f), data accuracy "
                    "= %.3e pix (intrinsic mean error %.3e)]", nid,
                    dupes ? "(non-unique!) " : "unique ",
                    scale*1800.*(xscale+yscale), angle, radius, aRadius, accuracy,
                    accuracy0);
      radius /= 1.25; /* next loop with smaller radius, if necessary */
    } while (dupes);
  } else {
    int n_matches = 0.8 * ndet;
    double dist_cut = 1e-3;      /* cut on accuracy of quad area ratios */
    cpl_msg_debug(__func__, "calling muse_ppm_kdmatch_point()");
    aid = muse_ppm_kdmatch_points(data, 120, patt, 150, dist_cut, aRadius, n_matches);
    nid = cpl_array_get_size(aid) - cpl_array_count_invalid(aid);
  }
  cpl_matrix_delete(data);
  cpl_matrix_delete(patt);

  /* add new columns for associated sky coordinates, erase *
   * pre-existing ones of the same name, if they exist     */
  if (cpl_table_has_column(detected, "RA")) {
    cpl_table_erase_column(detected, "RA");
  }
  cpl_table_new_column(detected, "RA", CPL_TYPE_DOUBLE);
  if (cpl_table_has_column(detected, "DEC")) {
    cpl_table_erase_column(detected, "DEC");
  }
  cpl_table_new_column(detected, "DEC", CPL_TYPE_DOUBLE);

  /* create matrices again for cpl_wcs_platesol(), this    *
   * time with detected pixel positions and sky positions, *
   * but only for the identified detections                */
  cpl_matrix *mpx = cpl_matrix_new(nid, 2),
             *msky = cpl_matrix_new(nid, 2);
  int iid = 0; /* index of identified object in matrices */
  for (i = 0; i < cpl_array_get_size(aid); i++) { /* index in reference table */
    if (!cpl_array_is_valid(aid, i)) {
      continue;
    }
    int idata = cpl_array_get_int(aid, i, NULL); /* index in detections table */
    double ra = cpl_table_get(aReference, "RA", i, NULL),
           dec = cpl_table_get(aReference, "DEC", i, NULL);
    cpl_matrix_set(mpx, iid, 0, cpl_table_get(detected, "XPOS", idata, NULL));
    cpl_matrix_set(mpx, iid, 1, cpl_table_get(detected, "YPOS", idata, NULL));
    cpl_matrix_set(msky, iid, 0, ra);
    cpl_matrix_set(msky, iid, 1, dec);
#if 0
    printf("matrices: %2d\t%.7f %.7f\t%6.2f %6.2f\n", iid + 1, ra, dec,
           cpl_table_get(detected, "XPOS", idata, NULL),
           cpl_table_get(detected, "YPOS", idata, NULL));
#endif
    cpl_table_set(detected, "RA", idata, ra);
    cpl_table_set(detected, "DEC", idata, dec);
    iid++;
  }
#if 0
  printf("mpx:\n");
  cpl_matrix_dump(mpx, stdout);
  printf("msky:\n");
  cpl_matrix_dump(msky, stdout);
  fflush(stdout);
#endif
  cpl_array_delete(aid);

  /* compute the solution */
  cpl_propertylist *wcsout = NULL;
  cpl_error_code rc = cpl_wcs_platesol(wcsin, msky, mpx, aIter, aThresh,
                                       CPL_WCS_PLATESOL_6, CPL_WCS_MV_CRVAL,
                                       &wcsout);
  if (aWCSObj->cube) {
    cpl_propertylist_copy_property_regexp(wcsout, aWCSObj->cube->header,
                                          CPL_WCS_REGEXP, 1);
  } /* if cube */
  cpl_matrix_delete(mpx);
  cpl_matrix_delete(msky);
  cpl_propertylist_delete(wcsin);
  if (rc != CPL_ERROR_NONE) {
    cpl_msg_warning(__func__, "Computing the WCS solution returned an error "
                    "(%d): %s", rc, cpl_error_get_message());
  }

  /* Compute the rotation angle and scales */
  double cd11 = muse_pfits_get_cd(wcsout, 1, 1),
         cd22 = muse_pfits_get_cd(wcsout, 2, 2),
         cd12 = muse_pfits_get_cd(wcsout, 1, 2),
         cd21 = muse_pfits_get_cd(wcsout, 2, 1),
         det = cd11 * cd22 - cd12 * cd21;
  if (det < 0.) {
    cd12 *= -1;
    cd11 *= -1;
  }
  /* the values we want, by default for non-rotation */
  double xang, yang;
  muse_wcs_get_angles(wcsout, &xang, &yang);
  muse_wcs_get_scales(wcsout, &xscale, &yscale);
  xscale *= 3600.; /* scales in arc seconds */
  yscale *= 3600.;
  cpl_msg_info(__func__, "astrometric calibration results: scales %f/%f "
               "arcsec/spaxel, rotation %g/%g deg", xscale, yscale, xang, yang);

#if 0
  printf("%s: output propertylist (rc = %d):\n", __func__, rc);
  fflush(stdout);
  cpl_propertylist_save(wcsout, "astrometry_wcsout.fits", CPL_IO_CREATE);
  system("fold -w 80 astrometry_wcsout.fits | grep -v \"^ \"");
  remove("astrometry_wcsout.fits");
#endif

  /* number of stars input to the astrometric fit as QC */
  cpl_propertylist_update_int(wcsout, QC_ASTROMETRY_NSTARS, nid);
  /* scales for QC in arcsec */
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_SCX, xscale);
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_SCY, yscale);
  /* angles in degrees */
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_ANGX, xang);
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_ANGY, yang);
  /* copy the "systematic error" as residuals for QC */
  double medresx = cpl_propertylist_get_double(wcsout, "CSYER1") * 3600.,
         medresy = cpl_propertylist_get_double(wcsout, "CSYER2") * 3600.;
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_RESX, medresx);
  cpl_propertylist_update_float(wcsout, QC_ASTROMETRY_RESY, medresy);

  /* update header with the final values used, just for information */
  cpl_propertylist_update_float(wcsout, MUSE_HDR_WCS_RADIUS, radius);
  cpl_propertylist_set_comment(wcsout, MUSE_HDR_WCS_RADIUS,
                               MUSE_HDR_WCS_RADIUS_C);
  if (accuracy > 0) {
    cpl_propertylist_update_float(wcsout, MUSE_HDR_WCS_ACCURACY, accuracy);
    cpl_propertylist_set_comment(wcsout, MUSE_HDR_WCS_ACCURACY,
                                 MUSE_HDR_WCS_ACCURACY_C);
    cpl_propertylist_update_float(wcsout, MUSE_HDR_WCS_FACCURACY,
                                  accuracy / accuracy0);
    cpl_propertylist_set_comment(wcsout, MUSE_HDR_WCS_FACCURACY,
                                 MUSE_HDR_WCS_FACCURACY_C);
  }

  aWCSObj->wcs = wcsout;
  return rc;
} /* muse_wcs_solve() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Find an optimal astrometry solution given a detection image.
  @param   aWCSObj      the MUSE WCS object
  @param   aDetSigma    the (negative!) sigma level used for object detection
  @param   aCentroid    type of centroiding algorithm to use
  @param   aReference   reference coordinates for the same field
  @param   aRadius      initial radius for pattern matching in pixels
  @param   aFAccuracy   factor to set initial accuracy relative to mean position
                        accuracy in aWCSObj->detected
  @param   aIter        number of iterations
  @param   aRejSigma    the rejection threshold
  @return  CPL_ERROR_NONE on success, another CPL error code on failure

  @note aWCSObj has to contain the cube with the associated detection image, as
        produced by @ref muse_wcs_locate_sources().
  @remark  The propertylist returned in aWCSObj->wcs contains the world
           coordinate solution.

  Repeatedly call muse_wcs_centroid_stars() and muse_wcs_solve() to find the
  best detection sigma level for the most accurate astrometric solution. The
  repeated measurements start with a detection sigma of |aDetSigma| and loop
  down to the 1.0-sigma level.

  The best solution is identified, as the one that gives the highest value of

    nid / 50. * min(medrms_x) / medrms_x * min(medrms_y) / medrms_y

  where nid is the identified number of stars, 50 is something of a lower limit
  for the useful number of stars, and medrms_[xy] are the median scatter left
  in the solution in x and y.

  The final detection sigma value is recorded in the header keyword
  MUSE_HDR_WCS_DETSIGMA.

  @note This function changes the CPL messaging level and is hence not supposed
        to be used from parallel code.

  @error{return CPL_ERROR_NULL_INPUT,
         input WCS object or its cube component are NULL}
  @error{return CPL_ERROR_DATA_NOT_FOUND,
         the astrometric detection image is missing from the cube}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aDetSigma is not negative}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         an unknown centroiding type was requested}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         table of reference coordinates is empty or NULL}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         table of detected or reference coordinates don't contain the expected columns}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         aRadius and/or aFAccuracy are non-positive}
  @error{return CPL_ERROR_ILLEGAL_OUTPUT,
         no objects could be identified at any sigma level}
  @error{propagate return code of muse_wcs_solve(),
         the solution with the finally selected detection sigma failed}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_optimize_solution(muse_wcs_object *aWCSObj, float aDetSigma,
                           muse_wcs_centroid_type aCentroid,
                           cpl_table *aReference, float aRadius,
                           float aFAccuracy, int aIter, float aRejSigma)
{
  cpl_ensure_code(aWCSObj && aWCSObj->cube, CPL_ERROR_NULL_INPUT);
  /* check that the cube contains the detection image */
  cpl_ensure_code(cpl_array_get_size(aWCSObj->cube->recnames) > 1 &&
                  !strncmp(cpl_array_get_string(aWCSObj->cube->recnames, 1),
                           MUSE_WCS_DETIMAGE_EXTNAME,
                           strlen(MUSE_WCS_DETIMAGE_EXTNAME) + 1),
                  CPL_ERROR_DATA_NOT_FOUND);
  cpl_ensure_code(aDetSigma < 0., CPL_ERROR_ILLEGAL_INPUT);
  switch (aCentroid) {
  case MUSE_WCS_CENTROID_GAUSSIAN:
  case MUSE_WCS_CENTROID_MOFFAT:
  case MUSE_WCS_CENTROID_BOX:
    break;
  default:
    return cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
  }
  int nref = cpl_table_get_nrow(aReference);
  cpl_ensure_code(nref > 0, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(muse_cpltable_check(aReference, muse_wcs_reference_def)
                  == CPL_ERROR_NONE, CPL_ERROR_BAD_FILE_FORMAT);
  cpl_ensure_code(aRadius > 0.&& aFAccuracy >= 0., CPL_ERROR_ILLEGAL_INPUT);

  float detsigma = fabsf(aDetSigma),
        lolimit = 0.9999; /* 1.0 with a bit of margin */
  muse_image *detimage = muse_imagelist_get(aWCSObj->cube->recimages, 1);

  /* clear incoming detections and solutions (if any) */
  cpl_table_delete(aWCSObj->detected);
  aWCSObj->detected = NULL;
  cpl_propertylist_delete(aWCSObj->wcs);
  aWCSObj->wcs = NULL;
  /* create table that can be used to find the optimal solution afterwards */
  cpl_table *tres = cpl_table_new(lround((detsigma - lolimit) / 0.1) + 1);
  cpl_table_new_column(tres, "detsigma", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "detsigma", "%.3f");
  cpl_table_new_column(tres, "ndet", CPL_TYPE_INT);
  cpl_table_new_column(tres, "nid", CPL_TYPE_INT);
  cpl_table_new_column(tres, "scalex", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "scalex", "%.4f");
  cpl_table_new_column(tres, "scaley", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "scaley", "%.4f");
  cpl_table_new_column(tres, "anglex", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "anglex", "%.3f");
  cpl_table_new_column(tres, "angley", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "angley", "%.3f");
  cpl_table_new_column(tres, "medresx", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "medresx", "%.3f");
  cpl_table_new_column(tres, "medresy", CPL_TYPE_FLOAT);
  cpl_table_set_column_format(tres, "medresy", "%.3f");

  /* run detection and solution for all sigma levels between |detsigma| and 1.0 */
  cpl_error_code rc = CPL_ERROR_NONE;
  float ds;
  int irow;
  for (ds = detsigma, irow = 0; ds > lolimit; ds -= 0.1, irow++) {
    cpl_msg_debug(__func__, "searching for solution with detection sigma %.3f", ds);
    cpl_msg_indent_more();
    /* quieten output from cpl_ppm_match_points(),d muse_wcs_centroid_stars(), *
     * etc.; we'll get the necessary output at the end with the final run      */
    cpl_msg_severity level = cpl_msg_get_level();
    cpl_msg_set_level(CPL_MSG_WARNING);

    /* first, detect stars at this level */
    aWCSObj->detected = muse_wcs_centroid_stars(detimage, ds, aCentroid);
    cpl_table_set_float(tres, "detsigma", irow, ds);
    cpl_table_set_int(tres, "ndet", irow, cpl_table_get_nrow(aWCSObj->detected));

    /* now see, if we identify stars and find a solution */
    cpl_errorstate state = cpl_errorstate_get();
    rc = muse_wcs_solve(aWCSObj, aReference, aRadius, aFAccuracy, aIter,
                        aRejSigma);
    if (rc == CPL_ERROR_NONE && aWCSObj->wcs) {
      cpl_table_set_int(tres, "nid", irow,
                        cpl_propertylist_get_int(aWCSObj->wcs, QC_ASTROMETRY_NSTARS));
      cpl_table_set_float(tres, "scalex", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_SCX));
      cpl_table_set_float(tres, "scaley", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_SCY));
      cpl_table_set_float(tres, "anglex", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_ANGX));
      cpl_table_set_float(tres, "angley", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_ANGY));
      cpl_table_set_float(tres, "medresx", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_RESX));
      cpl_table_set_float(tres, "medresy", irow,
                          cpl_propertylist_get_float(aWCSObj->wcs, QC_ASTROMETRY_RESY));
      cpl_propertylist_delete(aWCSObj->wcs);
      aWCSObj->wcs = NULL;
    } else {
      cpl_errorstate_set(state); /* ignore errors at this point */
    }
    cpl_table_delete(aWCSObj->detected);
    aWCSObj->detected = NULL;
    cpl_msg_set_level(level);
    cpl_msg_indent_less();
  } /* for ds */

  cpl_boolean debug = getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) >= 1;
  if (debug) {
    printf("%s: full table of results:\n", __func__);
    cpl_table_dump(tres, 0, 1000, stdout);
    fflush(stdout);
  }

  /* reject failed results and those far away from the expected scale */
  double scale = muse_pfits_get_mode(aWCSObj->cube->header) == MUSE_MODE_NFM_AO_N
               ? kMuseSpaxelSizeX_NFM : kMuseSpaxelSizeY_WFM;
  cpl_msg_debug(__func__, "pruning results +/-10%% away from expected spaxel "
                "scale of %.3f arcsec/pixel", scale);
  cpl_table_unselect_all(tres);
  cpl_table_or_selected_float(tres, "scalex", CPL_GREATER_THAN, scale * 1.1);
  cpl_table_or_selected_float(tres, "scalex", CPL_LESS_THAN, scale * 0.9);
  cpl_table_or_selected_float(tres, "scaley", CPL_GREATER_THAN, scale * 1.1);
  cpl_table_or_selected_float(tres, "scaley", CPL_LESS_THAN, scale * 0.9);
  cpl_table_or_selected_invalid(tres, "nid"); /* any of the result columns ... */
  cpl_table_erase_selected(tres);
  if (debug) {
    printf("%s: pruned table of results:\n", __func__);
    cpl_table_dump(tres, 0, 1000, stdout);
    fflush(stdout);
  }
  /* check if there are still table rows left */
  if (cpl_table_get_nrow(tres) < 1) {
    cpl_table_delete(tres);
    cpl_msg_error(__func__, "No valid solution found in the range %.3f .. %.3f "
                  "sigma", detsigma, lolimit);
    return cpl_error_set(__func__, CPL_ERROR_ILLEGAL_OUTPUT);
  }

  /* add table column with a weight to evaluate the goodness of each result; *
   * use                                                                     *
   *   NID / 50. * min(medresx) / medresx * min(medresy) / medresy           *
   * as formula to fill it                                                   */
  cpl_table_cast_column(tres, "nid", "weight", CPL_TYPE_DOUBLE);
  cpl_table_set_column_format(tres, "weight", "%.5g");
  cpl_table_divide_scalar(tres, "weight", 50.);
  cpl_table_divide_columns(tres, "weight", "medresx");
  cpl_table_multiply_scalar(tres, "weight",
                            cpl_table_get_column_min(tres, "medresx"));
  cpl_table_divide_columns(tres, "weight", "medresy");
  cpl_table_multiply_scalar(tres, "weight",
                            cpl_table_get_column_min(tres, "medresy"));
  cpl_propertylist *sort = cpl_propertylist_new();
  cpl_propertylist_append_bool(sort, "weight", CPL_TRUE);
  cpl_propertylist_append_bool(sort, "nid", CPL_TRUE);
  cpl_table_sort(tres, sort);
  cpl_propertylist_delete(sort);
  /* the best detection sigma is now the one at the top of the table */
  detsigma = cpl_table_get_float(tres, "detsigma", 0, NULL);
  if (debug) {
    printf("%s: pruned and sorted table of results:\n", __func__);
    cpl_table_dump(tres, 0, 1000, stdout);
    printf("%s: ===> use the %.3f-sigma level\n", __func__, detsigma);
    fflush(stdout);
  }
  cpl_table_delete(tres);

  /* so, run detection and solution one last time */
  aWCSObj->detected = muse_wcs_centroid_stars(detimage, detsigma, aCentroid);
  rc = muse_wcs_solve(aWCSObj, aReference, aRadius, aFAccuracy, aIter, aRejSigma);

  /* update header with the final value used, for information */
  if (aWCSObj->wcs) {
    cpl_propertylist_update_float(aWCSObj->wcs, MUSE_HDR_WCS_DETSIGMA, detsigma);
    cpl_propertylist_set_comment(aWCSObj->wcs, MUSE_HDR_WCS_DETSIGMA,
                                 MUSE_HDR_WCS_DETSIGMA_C);
  }

  return rc;
} /* muse_wcs_optimize_solution() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Create FITS headers containing a default (relative) WCS.
  @param   aHeader   optional input header containing instrument mode
                     (can be NULL)
  @return  A cpl_propertylist * on success, failures are not expected to occur.

  Units in the WCS are in deg (as required by the FITS standard, §8.2),
  approximately relative to a MUSE field center. Rotation is not known in this
  function, so assumed to be zero. This needs to be adapted when applying this
  WCS to data.

  If aHeader was not NULL, then that propertylist is queried for the INS.MODE.
  Depending on the outcome, the scale will be adapted to either WFM (the
  default, also if aHeader is NULL) or NFM.
 */
/*----------------------------------------------------------------------------*/
cpl_propertylist *
muse_wcs_create_default(const cpl_propertylist *aHeader)
{
  /* try getting the instrument mode; if no header is present or    *
   * it does not contain the INS.MODE keyword, this detaults to WFM */
  cpl_errorstate state = cpl_errorstate_get();
  cpl_boolean iswfm = muse_pfits_get_mode(aHeader) <= MUSE_MODE_WFM_AO_N;
  if (!cpl_errorstate_is_equal(state)) {
    cpl_errorstate_set(state);
  } /* if */

  /* create output header */
  cpl_propertylist *wcs = cpl_propertylist_new();

  /* We only care about the spatial axes, pretend that we have a 2D image;    *
   * set WCSAXES to make fitsverify happy when it finds the other ^C headers. */
  cpl_propertylist_append_int(wcs, "WCSAXES", 2);

  /* Check, if we are dealing with wcslib >= 4.23, which has the fix with the *
   * floating point numbers. If an older version is found, use a small number *
   * instead of zero for CRPIX, so that on loading CPL sees it as float value. */
  double smallvalue = FLT_MIN;
  const char *cpldesc = cpl_get_description(CPL_DESCRIPTION_DEFAULT); /* never fails */
  /* search for the WCSLIB version string */
  char *pcpldesc = strstr(cpldesc, "WCSLIB = ");
  if (pcpldesc) {
    pcpldesc += 8;
    double wcslibversion = atof(pcpldesc);
    if (wcslibversion >= 4.23) {
      smallvalue = 0.;
    } /* if newer wcslib */
  } /* if wcslib string found */

  /* axis 1 */
  /* CRPIX is the zero order correction to the astrometry, set zero here */
  cpl_propertylist_append_double(wcs, "CRPIX1", smallvalue);
  /* negative value, to signify that east is to the left */
  if (iswfm) {
    cpl_propertylist_append_double(wcs, "CD1_1", -kMuseSpaxelSizeX_WFM / 3600.);
  } else {
    cpl_propertylist_append_double(wcs, "CD1_1", -kMuseSpaxelSizeX_NFM / 3600.);
  }
  cpl_propertylist_append_string(wcs, "CTYPE1", "RA---TAN");
  cpl_propertylist_append_string(wcs, "CUNIT1", "deg");

  /* axis 2 */
  cpl_propertylist_append_double(wcs, "CRPIX2", smallvalue);
  if (iswfm) {
    cpl_propertylist_append_double(wcs, "CD2_2", kMuseSpaxelSizeY_WFM / 3600.);
  } else {
    cpl_propertylist_append_double(wcs, "CD2_2", kMuseSpaxelSizeY_NFM / 3600.);
  }
  cpl_propertylist_append_string(wcs, "CTYPE2", "DEC--TAN");
  cpl_propertylist_append_string(wcs, "CUNIT2", "deg");

  /* cross-terms are assumed to be not present (no rotation known!) */
  cpl_propertylist_append_double(wcs, "CD1_2", 0.);
  cpl_propertylist_append_double(wcs, "CD2_1", 0.);

  /* leave out CRVAL1/2, as we use this only to do the gnomonic projection */

  return wcs;
} /* muse_wcs_create_default() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Apply the CDi_j matrix of an astrometric solution to an observation.
  @param   aExpHeader   the header of the observation
  @param   aCalHeader   the header giving the astrometric solution
  @return  the header with CDi_j matrix that merges both rotations or NULL on
           error.

  This function uses matrix multiplication to merge position angle on the sky
  with the astrometric solution.
  The main output are the four spatial CDi_j entries in the output propertylist.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aCalHeader and/or aExpHeader are NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_propertylist *
muse_wcs_apply_cd(const cpl_propertylist *aExpHeader,
                  const cpl_propertylist *aCalHeader)
{
  cpl_ensure(aCalHeader && aExpHeader, CPL_ERROR_NULL_INPUT, NULL);

  /* duplicate the input WCS because we want to adapt it to the current data */
  cpl_propertylist *header = cpl_propertylist_duplicate(aCalHeader);

  /* Find field rotation (in radians) and create a CD matrix that reflects *
   * the exposure position angle corrected by the astrometric solution.    */
  double pa = muse_astro_posangle(aExpHeader) * CPL_MATH_RAD_DEG,
         xang, yang, xsc, ysc;
  muse_wcs_get_scales(header, &xsc, &ysc);
  muse_wcs_get_angles(header, &xang, &yang);
  cpl_msg_debug(__func__, "WCS solution: scales %f / %f arcsec, angles %f / %f "
                "deg", xsc * 3600., ysc * 3600., xang, yang);
  /* create PCi_j matrix from the CDi_j in the header and invert it */
  cpl_matrix *pc = cpl_matrix_new(2, 2);
  cpl_matrix_set(pc, 0, 0, muse_pfits_get_cd(header, 1, 1) / xsc);
  cpl_matrix_set(pc, 0, 1, muse_pfits_get_cd(header, 1, 2) / xsc);
  cpl_matrix_set(pc, 1, 0, muse_pfits_get_cd(header, 2, 1) / ysc);
  cpl_matrix_set(pc, 1, 1, muse_pfits_get_cd(header, 2, 2) / ysc);
  cpl_matrix *pcinv = cpl_matrix_invert_create(pc);
  cpl_matrix_delete(pc);
  /* now create corrective CDi_j elements from the inverted PCi_j matrix */
  double cd11cor, cd12cor, cd21cor, cd22cor;
  if (pcinv) {
    cd11cor = xsc * cpl_matrix_get(pcinv, 0, 0);
    cd12cor = xsc * cpl_matrix_get(pcinv, 0, 1);
    cd21cor = ysc * cpl_matrix_get(pcinv, 1, 0);
    cd22cor = ysc * cpl_matrix_get(pcinv, 1, 1);
    cpl_matrix_delete(pcinv);
  } else {
    cpl_msg_warning(__func__, "CD matrix of astrometric solution could not "
                    "be inverted! Assuming negligible zeropoint rotation.");
    cd11cor = xsc * 1.;
    cd12cor = xsc * 0.;
    cd21cor = ysc * 0.;
    cd22cor = ysc * 1.;
  }
  /* now we can finally compute the effective CDi_j of the exposure */
  double cd11 = cos(pa) * cd11cor - sin(pa) * cd21cor,
         cd12 = cos(pa) * cd12cor - sin(pa) * cd22cor,
         cd21 = sin(pa) * cd11cor + cos(pa) * cd21cor,
         cd22 = sin(pa) * cd12cor + cos(pa) * cd22cor;
  cpl_propertylist_update_double(header, "CD1_1", cd11),
  cpl_propertylist_update_double(header, "CD1_2", cd12),
  cpl_propertylist_update_double(header, "CD2_1", cd21);
  cpl_propertylist_update_double(header, "CD2_2", cd22),
  muse_wcs_get_scales(header, &xsc, &ysc);
  muse_wcs_get_angles(header, &xang, &yang);
  cpl_msg_debug(__func__, "Updated CD matrix (%f deg field rotation): "
                "%e %e %e %e (scales %f / %f arcsec, angles %f / %f deg)",
                pa * CPL_MATH_DEG_RAD, cd11, cd12, cd21, cd22,
                xsc * 3600., ysc * 3600., xang, yang);
  return header;
} /* muse_wcs_apply_cd() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Carry out a gnomonic projection of a pixel table into native
           spherical coordinates.
  @param   aPixtable   the input pixel table containing the exposure to be
                       corrected
  @param   aWCS        FITS headers containing the WCS solution
  @return  CPL_ERROR_NONE for success, any other value for failure
  @remark  The resulting WCS transform is directly applied and returned in the
           input pixel table, changing units of the spatial columns from "pix"
           to "rad". The table column MUSE_PIXTABLE_XPOS is transformed to phi
           and MUSE_PIXTABLE_YPOS to theta - pi/2.
  @remark This function adds a FITS header (@ref MUSE_HDR_PT_WCS) with a
          string value (@ref MUSE_HDR_PT_WCS_PROJ) to the pixel table, for
          information.

  Loop through the input pixel table and evaluate the input WCS at every pixel
  to find the correct position for a given input position. This uses standard
  WCS transforms to produce "native spherical" coordinates in gnomonic (TAN)
  projection as defined in Calabretta & Greisen 2002 A&A 395, 1077 (Paper II)
  for the exposure in the pixel table.

  @error{return CPL_ERROR_NULL_INPUT,
         the input pixel table\, its header\, or the input WCS headers object are NULL}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         the input pixel table WCS type is not pixel-based}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         the input WCS is not for a gnomonic (TAN) projection}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_project_tan(muse_pixtable *aPixtable, const cpl_propertylist *aWCS)
{
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  /* nrow == 0 implies a NULL or broken pixel table */
  cpl_ensure_code(nrow > 0 && aPixtable->header && aWCS, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_PIXEL,
                  CPL_ERROR_INCOMPATIBLE_INPUT);
  /* ensure that the input WCS is for gnomonic projection */
  const char *type1 = muse_pfits_get_ctype(aWCS, 1),
             *type2 = muse_pfits_get_ctype(aWCS, 2);
  cpl_ensure_code(type1 && type2 && !strncmp(type1, "RA---TAN", 9) &&
                  !strncmp(type2, "DEC--TAN", 9),
                  CPL_ERROR_UNSUPPORTED_MODE);

  /* clean main WCS keys from input pixel table, in *
   * case there are keys we do not overwrite below  */
  cpl_propertylist_erase_regexp(aPixtable->header, MUSE_WCS_KEYS, 0);

  /* apply the exposure rotation on top of the zeropoint *
   * rotation from the astrometric calibration           */
  cpl_propertylist *header = muse_wcs_apply_cd(aPixtable->header, aWCS);
  /* don't want CRVAL or LON/LATPOLE in the output, *
   * because we create native coords here           */
  cpl_propertylist_erase_regexp(header, "^CRVAL[0-9]+$|^L[OA][NT]POLE$", 0);
  double cd11 = muse_pfits_get_cd(header, 1, 1),
         cd12 = muse_pfits_get_cd(header, 1, 2),
         cd21 = muse_pfits_get_cd(header, 2, 1),
         cd22 = muse_pfits_get_cd(header, 2, 2);

  /* Compute reference pixel from center of the data plus the zero  *
   * order correction of the corrective WCS (CRPIX1/2 of aWCS); use *
   * the spatial extents before the DAR correction, if possible.    */
  cpl_errorstate prestate = cpl_errorstate_get();
  double xlo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XLO),
         xhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_PREDAR_XHI),
         ylo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YLO),
         yhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_PREDAR_YHI);
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_errorstate_set(prestate);
    /* try normal pixel table headers now */
    xlo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XLO);
    xhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XHI);
    ylo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YLO);
    yhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YHI);
  }
  double wcspix1 = muse_pfits_get_crpix(header, 1),
         wcspix2 = muse_pfits_get_crpix(header, 2),
         crpix1 = (xhi + xlo) / 2. + wcspix1,
         crpix2 = (yhi + ylo) / 2. + wcspix2;
  cpl_propertylist_update_double(header, "CRPIX1", crpix1),
  cpl_propertylist_update_double(header, "CRPIX2", crpix2),
  cpl_msg_debug(__func__, "Using reference pixel %f/%f (limits in pixel table "
                "%f..%f/%f..%f, WCS correction %f,%f)", crpix1, crpix2,
                xlo, xhi, ylo, yhi, wcspix1, wcspix2);

  /* delete the units of the x/y columns while we are working on them */
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS, "");
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_YPOS, "");

  /* replace coordinate values in the pixel table by the computed ones, *
   * carry out the _partial_ WCS coordinate transform for each pixel    */
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS);
  cpl_size n;
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(cd11, cd12, cd21, cd22, crpix1, crpix2, nrow, xpos, ypos)
  for (n = 0; n < nrow; n++) {
    /* conversion from pixel coordinates to projection plane coordinates;   *
     * x,y as in Calabretta & Greisen 2002 A&A 395, 1077 (Paper II), Fig. 1 */
    double x = cd11 * (xpos[n] - crpix1) + cd12 * (ypos[n] - crpix2),
           y = cd22 * (ypos[n] - crpix2) + cd21 * (xpos[n] - crpix1);

    /* conversion from projection plane to native spherical coordinates: *
     * phi,theta as in Calabretta & Greisen 2002 (Paper II), Fig. 1;     *
     * these formulae are for the gnomomic (TAN) case, Sect. 5.1/5.1.3,  *
     * i.e. Eq. (14), (15), and (55)                                     *
     * As we only further use these in other parts of the pipeline, the  *
     * values are not converted to degrees but stay in radians.          */
    double phi = atan2(x, -y),
           theta = atan(CPL_MATH_DEG_RAD / sqrt(x*x + y*y));
    if (phi < 0) { /* phi should be between 0 and 2pi, to let tests pass */
      phi += CPL_MATH_2PI;
    }

    /* conversion from native spherical to celestial spherical coordinates *
     * is done later when combining exposures, see muse_xcombine_tables()  */
    xpos[n] = phi;
    ypos[n] = theta - CPL_MATH_PI_2; /* subtract pi/2 for better accuracy */
  } /* for n (table/matrix rows) */

  /* Here we produce units in radians; use "rad" unit string to signify  *
   * that these are native spherical coordinates but haven't gotten full *
   * WCS treatment to alpha,delta yet                                    */
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS, "rad");
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_YPOS, "rad");
  muse_pixtable_compute_limits(aPixtable);
  /* copy spatial WCS to the pixel table */
  cpl_propertylist_copy_property_regexp(aPixtable->header, header,
                                        MUSE_WCS_KEYS, 0);
  cpl_propertylist_delete(header);

  /* add the status header */
  cpl_propertylist_update_string(aPixtable->header, MUSE_HDR_PT_WCS,
                                 MUSE_HDR_PT_WCS_PROJ);
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_WCS,
                               MUSE_HDR_PT_WCS_COMMENT_PROJ);
  return CPL_ERROR_NONE;
} /* muse_wcs_project_tan() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert native to celestial spherical coordinates in a pixel table.
  @param   aPixtable   the input pixel table containing the exposure to be
                       converted
  @param   aRA         the reference right-ascension in degrees
  @param   aDEC        the reference declination in degrees
  @return  CPL_ERROR_NONE for success, any other value for failure
  @remark  The resulting WCS transform is directly applied and returned in the
           input pixel table, changing units of the spatial columns from
           "rad" to "deg". MUSE_PIXTABLE_XPOS is transformed to RA and
           MUSE_PIXTABLE_YPOS to DEC.
  @remark This function updates a FITS header (@ref MUSE_HDR_PT_WCS) with a
          string value (@ref MUSE_HDR_PT_WCS_POSI) to the pixel table, for
          information.

  Loop through the input pixel table and evaluate the input WCS at every pixel
  to find the correct celestial coordinate for a given input position. This
  uses standard coordinate transforms as defined in Calabretta & Greisen 2002
  A&A 395, 1077 (Paper II) for the exposure in the pixel table. The output
  contains a full spatial WCS with "celestial spherical" coordinates.

  @error{return CPL_ERROR_NULL_INPUT,
         the input pixel table or its header are NULL}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         the input pixel table WCS type is not native spherical}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         the input WCS is not for a gnomonic (TAN) projection}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_position_celestial(muse_pixtable *aPixtable, double aRA, double aDEC)
{
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  /* nrow == 0 implies a NULL or broken pixel table */
  cpl_ensure_code(nrow > 0 && aPixtable->header, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_NATSPH,
                  CPL_ERROR_INCOMPATIBLE_INPUT);
  /* ensure that the input WCS is for gnomonic projection */
  const char *type1 = muse_pfits_get_ctype(aPixtable->header, 1),
             *type2 = muse_pfits_get_ctype(aPixtable->header, 2);
  cpl_ensure_code(type1 && type2 && !strncmp(type1, "RA---TAN", 9) &&
                  !strncmp(type2, "DEC--TAN", 9),
                  CPL_ERROR_UNSUPPORTED_MODE);

  cpl_msg_info(__func__, "Adapting WCS to RA/DEC=%f/%f deg", aRA, aDEC);

  /* delete the units of the x/y columns while we are working on them */
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS, "");
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_YPOS, "");

  /* replace coordinate values in the pixel table by the computed ones, *
   * carry out the spherical coordinate rotation for each pixel         */
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS);
  double dp = aDEC / CPL_MATH_DEG_RAD; /* delta_p in Paper II (in radians) */
  cpl_size n;
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aDEC, dp, nrow, xpos, ypos)
  for (n = 0; n < nrow; n++) {
    /* conversion from native spherical to celestial spherical coordinates   *
     * alpha,delta as in Calabretta & Greisen 2002 A&A 395, 1077 (Paper II), *
     * Fig. 1; the formulae used are Eq. (2) in Sect. 2.3 in that paper and  *
     * are generic but use that phi_p = 180 deg for zenithal projections     *
     * (like TAN), i.e. cos(phi - phi_p) = -cos(phi) and similar for sin.    */
    double phi = xpos[n],
           theta = ypos[n] + CPL_MATH_PI_2, /* add pi/2 again */
           ra = atan2(cos(theta) * sin(phi),
                      sin(theta) * cos(dp) + cos(theta) * sin(dp) * cos(phi))
              * CPL_MATH_DEG_RAD,
           dec = asin(sin(theta) * sin(dp) - cos(theta) * cos(dp) * cos(phi))
               * CPL_MATH_DEG_RAD;
    /* The following should be                                     *
     *    xpos = aRA + ra;                                         *
     *    ypos = dec;                                              *
     * but let's remove the zeropoint, to help accuracy despite    *
     * the limited float precision (~7 digits, i.e. ~0.36 arcsec). */
    xpos[n] = ra;
    ypos[n] = dec - aDEC;
  } /* for n (table/matrix rows) */

  /* We produce output units in degrees (like wcslib); signify *
   * full WCS transformation by setting the "deg" output unit  */
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS, "deg");
  cpl_table_set_column_unit(aPixtable->table, MUSE_PIXTABLE_YPOS, "deg");
  /* append CRVAL now that we have a full WCS */
  cpl_propertylist_update_double(aPixtable->header, "CRVAL1", aRA);
  cpl_propertylist_update_double(aPixtable->header, "CRVAL2", aDEC);
  /* do the limit recomputation after setting the CRVALs */
  muse_pixtable_compute_limits(aPixtable);

  /* add the status header */
  cpl_propertylist_update_string(aPixtable->header, MUSE_HDR_PT_WCS,
                                 MUSE_HDR_PT_WCS_POSI);
  cpl_propertylist_set_comment(aPixtable->header, MUSE_HDR_PT_WCS,
                               MUSE_HDR_PT_WCS_COMMENT_POSI);
  return CPL_ERROR_NONE;
} /* muse_wcs_position_celestial() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert from pixel coordinates to celestial spherical coordinates.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aX        the horizontal pixel position
  @param   aY        the vertical pixel position
  @param   aRA       the output right-ascension in degrees
  @param   aDEC      the output declination in degrees
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader is used to do the transformation,
  only the gnomonic (TAN) is supported.

  Uses Eqns (14), (15), (55), and (2) from Calabretta & Greisen 2002 A&A 395,
  1077 (Paper II). We use that phi_p = 180 deg for zenithal projections (like
  TAN).
  The equations are actually implemented in muse_wcs_celestial_from_pixel_fast()
  that is called by this function.

  @error{return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         aHeader does not contain gnomonic (TAN) WCS}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_celestial_from_pixel(cpl_propertylist *aHeader, double aX, double aY,
                              double *aRA, double *aDEC)
{
  cpl_ensure_code(aHeader && aRA && aDEC, CPL_ERROR_NULL_INPUT);
  const char *type1 = muse_pfits_get_ctype(aHeader, 1),
             *type2 = muse_pfits_get_ctype(aHeader, 2);
  cpl_ensure_code(type1 && type2 && !strncmp(type1, "RA---TAN", 9) &&
                  !strncmp(type2, "DEC--TAN", 9),
                  CPL_ERROR_UNSUPPORTED_MODE);

  muse_wcs *wcs = muse_wcs_new(aHeader);
  muse_wcs_celestial_from_pixel_fast(wcs, aX, aY, aRA, aDEC);
  cpl_free(wcs);

  return CPL_ERROR_NONE;
} /* muse_wcs_celestial_from_pixel() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert from celestial spherical coordinates to pixel coordinates.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aRA       the right-ascension in degrees
  @param   aDEC      the declination in degrees
  @param   aX        the output horizontal pixel position
  @param   aY        the output vertical pixel position
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader is used to do the transformation,
  only the gnomonic (TAN) projection is supported.

  Uses Eqns (5), (12), (13), and (54) from Calabretta & Greisen 2002 A&A 395,
  1077 (Paper II). We use that phi_p = 180 deg for zenithal projections (like
  TAN).
  The equations are actually implemented in muse_wcs_pixel_from_celestial_fast()
  that is called by this function.

  @error{set and return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{set and return CPL_ERROR_UNSUPPORTED_MODE,
         aHeader does not contain gnomonic (TAN) WCS}
  @error{set and return CPL_ERROR_SINGULAR_MATRIX\, set contents of aX and aY to NAN,
         determinant of the CDi_j matrix in aHeader is zero}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_pixel_from_celestial(cpl_propertylist *aHeader, double aRA, double aDEC,
                              double *aX, double *aY)
{
  cpl_ensure_code(aHeader && aX && aY, CPL_ERROR_NULL_INPUT);
  /* make sure that the header represents TAN */
  const char *type1 = muse_pfits_get_ctype(aHeader, 1),
             *type2 = muse_pfits_get_ctype(aHeader, 2);
  cpl_ensure_code(type1 && type2 && !strncmp(type1, "RA---TAN", 9) &&
                  !strncmp(type2, "DEC--TAN", 9),
                  CPL_ERROR_UNSUPPORTED_MODE);

  muse_wcs *wcs = muse_wcs_new(aHeader);
  if (wcs->cddet == 0.) { /* that's important here */
    *aX = *aY = NAN;
    cpl_error_set(__func__, CPL_ERROR_SINGULAR_MATRIX);
    cpl_free(wcs);
    return CPL_ERROR_SINGULAR_MATRIX;
  }
  wcs->crval1 /= CPL_MATH_DEG_RAD; /* convert to radians */
  wcs->crval2 /= CPL_MATH_DEG_RAD;
  muse_wcs_pixel_from_celestial_fast(wcs, aRA / CPL_MATH_DEG_RAD,
                                     aDEC / CPL_MATH_DEG_RAD, aX, aY);
  cpl_free(wcs);

  return CPL_ERROR_NONE;
} /* muse_wcs_pixel_from_celestial() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert from celestial spherical coordinates to projection plane
           coordinates.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aRA       the right-ascension in degrees
  @param   aDEC      the declination in degrees
  @param   aX        the output horizontal proj. plane position
  @param   aY        the output vertical proj. plane position
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader is used to do the transformation,
  only the gnomonic (TAN) is supported.

  Uses Eqns (5), (12), (13), and (54) from Calabretta & Greisen 2002 A&A 395,
  1077 (Paper II). We use that phi_p = 180 deg for zenithal projections (like
  TAN).

  XXX this duplicates most of the code of muse_wcs_pixel_from_celestial()

  @error{return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{return CPL_ERROR_UNSUPPORTED_MODE,
         aHeader does not contain gnomonic (TAN) WCS}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         determinant of the CDi_j matrix in aHeader is zero}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_projplane_from_celestial(cpl_propertylist *aHeader, double aRA,
                                  double aDEC, double *aX, double *aY)
{
  cpl_ensure_code(aHeader && aX && aY, CPL_ERROR_NULL_INPUT);
  /* make sure that the header represents TAN */
  const char *type1 = muse_pfits_get_ctype(aHeader, 1),
             *type2 = muse_pfits_get_ctype(aHeader, 2);
  cpl_ensure_code(type1 && type2 && !strncmp(type1, "RA---TAN", 9) &&
                  !strncmp(type2, "DEC--TAN", 9),
                  CPL_ERROR_UNSUPPORTED_MODE);

  /* spherical coordinate shift/translation */
  double a = aRA / CPL_MATH_DEG_RAD,  /* RA in radians */
         d = aDEC / CPL_MATH_DEG_RAD, /* DEC in radians */
         /* alpha_p and delta_p in Paper II (in radians) */
         ap = muse_pfits_get_crval(aHeader, 1) / CPL_MATH_DEG_RAD,
         dp = muse_pfits_get_crval(aHeader, 2) / CPL_MATH_DEG_RAD,
         phi = atan2(-cos(d) * sin(a - ap),
                     sin(d) * cos(dp) - cos(d) * sin(dp) * cos(a-ap))
             + 180 / CPL_MATH_DEG_RAD,
         theta = asin(sin(d) * sin(dp) + cos(d) * cos(dp) * cos(a-ap)),
         R_theta = CPL_MATH_DEG_RAD / tan(theta);
  /* spherical deprojection */
  *aX = R_theta * sin(phi),
  *aY = -R_theta * cos(phi);

  return CPL_ERROR_NONE;
} /* muse_wcs_projplane_from_celestial() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert from pixel coordinates to projection plane coordinates.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aX        the horizontal pixel position
  @param   aY        the vertical pixel position
  @param   aXOut     the output x position
  @param   aYOut     the output y position
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader is used to do the transformation.

  @note It is assumed that the header only contains a linear transformation, but
        no check is carried out for that.

  @error{return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_projplane_from_pixel(cpl_propertylist *aHeader, double aX, double aY,
                              double *aXOut, double *aYOut)
{
  cpl_ensure_code(aHeader && aXOut && aYOut, CPL_ERROR_NULL_INPUT);

  muse_wcs *wcs = muse_wcs_new(aHeader);
  muse_wcs_projplane_from_pixel_fast(wcs, aX, aY, aXOut, aYOut);
  cpl_free(wcs);

  return CPL_ERROR_NONE;
} /* muse_wcs_projplane_from_pixel() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Convert from projection plane coordinates to pixel coordinates.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aX        the x position
  @param   aY        the y position
  @param   aXOut     the output horizontal pixel position
  @param   aYOut     the output vertical pixel position
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader is used to do the transformation.

  @note It is assumed that the header only contains a linear transformation, but
        no check is carried out for that.

  @error{set and return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{set and return CPL_ERROR_SINGULAR_MATRIX\, set contents of aX and aY to NAN,
         determinant of the CDi_j matrix in aHeader is zero}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_pixel_from_projplane(cpl_propertylist *aHeader, double aX, double aY,
                              double *aXOut, double *aYOut)
{
  cpl_ensure_code(aHeader && aXOut && aYOut, CPL_ERROR_NULL_INPUT);

  muse_wcs *wcs = muse_wcs_new(aHeader);
  if (wcs->cddet == 0.) { /* that's important here */
    *aXOut = *aYOut = NAN;
    cpl_error_set(__func__, CPL_ERROR_SINGULAR_MATRIX);
    cpl_free(wcs);
    return CPL_ERROR_SINGULAR_MATRIX;
  }
  muse_wcs_pixel_from_projplane_fast(wcs, aX, aY, aXOut, aYOut);
  cpl_free(wcs);

  return CPL_ERROR_NONE;
} /* muse_wcs_pixel_from_projplane() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the rotation angles (in degrees) from the FITS header WCS.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aXAngle   the output angle in x-direction
  @param   aYAngle   the output angle in y-direction
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader, i.e. the CDi_j matrix, is used to
  compute the angles.

  References:
  - based on public domain code of the IDL astro-lib procedure getrot.pro
  - http://idlastro.gsfc.nasa.gov/ftp/pro/astrom/getrot.pro

  @error{return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{propagate the error of cpl_propertylist_get_double(),
         some elements of the CDi_j matrix don't exist in aHeader}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_get_angles(cpl_propertylist *aHeader, double *aXAngle, double *aYAngle)
{
  cpl_ensure_code(aHeader && aXAngle && aYAngle, CPL_ERROR_NULL_INPUT);

  cpl_errorstate prestate = cpl_errorstate_get();
  double cd11 = muse_pfits_get_cd(aHeader, 1, 1),
         cd22 = muse_pfits_get_cd(aHeader, 2, 2),
         cd12 = muse_pfits_get_cd(aHeader, 1, 2),
         cd21 = muse_pfits_get_cd(aHeader, 2, 1),
         det = cd11 * cd22 - cd12 * cd21;
  cpl_ensure_code(cpl_errorstate_is_equal(prestate), cpl_error_get_code());
  if (det < 0.) {
    cd12 *= -1;
    cd11 *= -1;
  }
  if (cd12 == 0. && cd21 == 0.) { /* matrix without rotation */
    *aXAngle = 0.;
    *aYAngle = 0.;
    return CPL_ERROR_NONE;
  }
  /* angles in degrees */
  *aXAngle = atan2(cd12, cd11)  * CPL_MATH_DEG_RAD;
  *aYAngle = atan2(-cd21, cd22) * CPL_MATH_DEG_RAD;
  return CPL_ERROR_NONE;
} /* muse_wcs_get_angles() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the spatial scales (in degrees) from the FITS header WCS.
  @param   aHeader   the input header containing the WCS of the exposure
  @param   aXScale   the output scale in x-direction
  @param   aYScale   the output scale in y-direction
  @return  CPL_ERROR_NONE for success, any other value for failure

  The world coordinate system from aHeader, i.e. the CDi_j matrix, is used to
  compute the scales.

  References:
  - based on public domain code of the IDL astro-lib procedure getrot.pro
  - http://idlastro.gsfc.nasa.gov/ftp/pro/astrom/getrot.pro

  @error{return CPL_ERROR_NULL_INPUT, aHeader\, aX\, and/or aY are NULL}
  @error{propagate the error of cpl_propertylist_get_double(),
         some elements of the CDi_j matrix don't exist in aHeader}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_get_scales(cpl_propertylist *aHeader, double *aXScale, double *aYScale)
{
  cpl_ensure_code(aHeader && aXScale && aYScale, CPL_ERROR_NULL_INPUT);

  cpl_errorstate prestate = cpl_errorstate_get();
  double cd11 = muse_pfits_get_cd(aHeader, 1, 1),
         cd22 = muse_pfits_get_cd(aHeader, 2, 2),
         cd12 = muse_pfits_get_cd(aHeader, 1, 2),
         cd21 = muse_pfits_get_cd(aHeader, 2, 1),
         det = cd11 * cd22 - cd12 * cd21;
  cpl_ensure_code(cpl_errorstate_is_equal(prestate), cpl_error_get_code());

  if (det < 0.) {
    cd12 *= -1;
    cd11 *= -1;
  }
  if (cd12 == 0. && cd21 == 0.) { /* matrix without rotation */
    *aXScale = cd11;
    *aYScale = cd22;
    return CPL_ERROR_NONE;
  }
  *aXScale = sqrt(cd11*cd11 + cd12*cd12); /* only the absolute value */
  *aYScale = sqrt(cd22*cd22 + cd21*cd21);
  return CPL_ERROR_NONE;
} /* muse_wcs_get_scales() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Create a new WCS structure from a given FITS header.
  @param   aHeader   the input header containing the input WCS
  @return  A newly allocated muse_wcs *.

  The world coordinate system from aHeader, i.e. the CDi_j matrix, the CRPIXi,
  CRVALi, are used to fill the structure. The iscelsph element of the structure
  is set to FALSE, it can be easily set using other means.

  The returned pointer has to be deallocated using cpl_free().

  @error{return the structure initialized with zeros\, except cd11 = cd22 = cddet = 1.,
         aHeader is NULL or does not contain the needed values}
  @error{set CPL_ERROR_SINGULAR_MATRIX\, but continue normally,
         the determinant of the CDi_j matrix (the cddet element) is zero}
 */
/*----------------------------------------------------------------------------*/
muse_wcs *
muse_wcs_new(cpl_propertylist *aHeader)
{
  muse_wcs *wcs = cpl_calloc(1, sizeof(muse_wcs));
  if (!aHeader) {
    wcs->cd11 = wcs->cd22 = wcs->cddet = 1.; /* see below */
    return wcs;
  }

  cpl_errorstate prestate = cpl_errorstate_get();
  wcs->crpix1 = muse_pfits_get_crpix(aHeader, 1);
  wcs->crpix2 = muse_pfits_get_crpix(aHeader, 2);
  wcs->crval1 = muse_pfits_get_crval(aHeader, 1);
  wcs->crval2 = muse_pfits_get_crval(aHeader, 2);
  if (!cpl_errorstate_is_equal(prestate)) {
    /* all these headers default to 0.0 following the FITS *
     * Standard, so we can ignore any errors set here      */
    cpl_errorstate_set(prestate);
  }

  prestate = cpl_errorstate_get();
  wcs->cd11 = muse_pfits_get_cd(aHeader, 1, 1);
  wcs->cd22 = muse_pfits_get_cd(aHeader, 2, 2);
  wcs->cd12 = muse_pfits_get_cd(aHeader, 1, 2);
  wcs->cd21 = muse_pfits_get_cd(aHeader, 2, 1);
  if (!cpl_errorstate_is_equal(prestate) &&
      wcs->cd11 == 0. && wcs->cd12 == 0. && wcs->cd21 == 0. && wcs->cd22 == 0.) {
    /* FITS Standard says to handle the CD matrix like the PC *
     * matrix in this case, with 1 for the diagonal elements  */
    wcs->cd11 = wcs->cd22 = wcs->cddet = 1.;
    cpl_errorstate_set(prestate); /* not a real error */
  }
  wcs->cddet = wcs->cd11 * wcs->cd22 - wcs->cd12 * wcs->cd21;
  if (wcs->cddet == 0.) {
    cpl_error_set(__func__, CPL_ERROR_SINGULAR_MATRIX);
  }
  /* wcs->iscelsph defaults to 0 = CPL_FALSE, leave it at that */

#if 0
  if (getenv("MUSE_DEBUG_WCS") && atoi(getenv("MUSE_DEBUG_WCS")) > 0) {
    cpl_msg_debug(__func__, "wcs: axis1 = { %f %f %e }, axis2 = { %f %f %e }, "
                  "crossterms = { %e %e }, det = %e",
                  wcs->crpix1, wcs->crval1, wcs->cd11,
                  wcs->crpix2, wcs->crval2, wcs->cd22,
                  wcs->cd12, wcs->cd21, wcs->cddet);
  }
#endif
  return wcs;
} /* muse_wcs_new() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Append WCS keywords to another header using a postfix.
  @param   aWCS       the header containing the WCS to be copied
  @param   aHeader    the header into which the keywords are copied
  @param   aPostfix   the postfix (space or 'A' to 'Z')
  @param   aWCSName   the WCSNAMEa keyword to set
  @return  CPL_ERROR_NONE for success, any other value for failure

  This function copies the existing WCS keywords, defined as in the regular
  expression @c CPL_WCS_REGEXP, from the aWCS to aHeader, using the given
  aPostfix to append the keyword postfix (see section "Alternative WCS Axis
  Descriptions" in the FITS Standard v3.0).

  This function only appends the new header entries, it does not check for
  duplication. So the caller needs to make sure that none of the appended
  keywords is a duplicate of an existing one.
  The caller also needs to make sure that the world coordinates in aWCS
  are appropriate for the destination header, i.e. that not a 2D WCS is copied
  into a header for a 3D object or vice versa. Finally, the caller should make
  sure that no other secondary WCS is present in aWCS, otherwise this gets
  copied (and mangled) as well.

  @error{set and return CPL_ERROR_NULL_INPUT, aWCS and/or aHeader is NULL}
  @error{propagate error code of cpl_propertylist_append(),
         copying the keywords fails}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_wcs_copy_keywords(const cpl_propertylist *aWCS, cpl_propertylist *aHeader,
                       char aPostfix, const char *aWCSName)
{
  cpl_ensure_code(aWCS && aHeader, CPL_ERROR_NULL_INPUT);

  /* copy WCS keywords from input header */
  cpl_propertylist *hwcs = cpl_propertylist_new();
  cpl_propertylist_copy_property_regexp(hwcs, aWCS, CPL_WCS_REGEXP, 0);
  /* add name to the new WCS, if one was given */
  if (aWCSName) {
    cpl_propertylist_update_string(hwcs, "WCSNAME", aWCSName);
  }

  /* rename all properties to include the given postfix */
  int ip, np = cpl_propertylist_get_size(hwcs);
  for (ip = 0; ip < np; ip++) {
    cpl_property *p = cpl_propertylist_get(hwcs, ip);
    char *newname = cpl_sprintf("%s%c", cpl_property_get_name(p), aPostfix);
#if 0
    cpl_msg_debug(__func__, "%s -> %s", cpl_property_get_name(p), newname);
#endif
    cpl_property_set_name(p, newname);
    cpl_free(newname);
  } /* for ip (all properties) */

  cpl_error_code rc = cpl_propertylist_append(aHeader, hwcs);
  cpl_errorstate state = cpl_errorstate_get();
  const char *name = muse_pfits_get_extname(aHeader);
  if (name) {
    cpl_msg_debug(__func__, "Appended WCS keywords to extension %s, using "
                  "keyword postfix %c", name, aPostfix);
  } else {
    cpl_errorstate_set(state);
    cpl_msg_debug(__func__, "Appended WCS keywords to unnamed extension, using "
                  "keyword postfix %c", aPostfix);
  }
  cpl_propertylist_delete(hwcs);

  return rc;
} /* muse_wcs_copy_keywords() */

/**@}*/
