/* -*- 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-2016 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

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

#include "muse_pixtable.h"
#include "muse_instrument.h"

#include "muse_cplwrappers.h"
#include "muse_geo.h"
#include "muse_mask.h"
#include "muse_pfits.h"
#include "muse_quadrants.h"
#include "muse_quality.h"
#include "muse_postproc.h"
#include "muse_tracing.h"
#include "muse_wavecalib.h"
#include "muse_wcs.h"
#include "muse_utils.h"

/*----------------------------------------------------------------------------*
 *                             Debugging Macros                               *
 *         Set these to 1 or higher for (lots of) debugging output            *
 *----------------------------------------------------------------------------*/
#define CREATE_MINIMAL_PIXTABLE 0 /* create a very small pixel table */
#define PIXTABLE_CREATE_CCDSIZED 1 /* start with pixel table sized like input image */
#define DEBUG_PIXTABLE_CREATION 0 /* some table dumps in muse_pixtable_create() */
#define DEBUG_PIXTABLE_FEW_SLICES 0 /* when doing quick pixel table creation for  *
                                     * debugging, set this to a value below 48    */
#define PIXTABLE_MASK_PARALLEL 0 /* make muse_pixtable_and_selected_mask() *
                                  * OpenMP-parallel (little gains)         */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_pixtable     Pixel table handling
 *
 * To be able to easily handle the muse_pixtable structure as defined in
 * DRLDesign Sect. 5, a number of functions are used in the MUSE DRL.
 *
 * @copydetails muse_pixtable_def
 *
 * The information in the "origin" column is optimized for RAM usage, encoding
 * several separate values into one 32bit integer. The offset of each slice of
 * each IFU is stored in a FITS keyword, then x coordinates can be stored
 * relative to that.
 *
 * Storage sizes needed for the numbers involved in the origin column:
 *
 * @verbatim
    + X per slice 1...~90 (use range 0... 127 -->  7 bits)
    + Y trimmed 1...4112  (use range 0...8191 --> 13 bits)
    + IFU 1...24          (use range 0...  31 -->  5 bits)
    + Slice 1...48        (use range 0...  63 -->  6 bits)
                                                  31 bits@endverbatim
 *
 * Bit usage in the origin column:
 *
 * @verbatim
   30  20                     10                  0
   1 0 9 8  7 6 5 4  3 2 1 0  9 8 7 6  5 4 3 2  1 0 9 8  7 6 5 4  3 2 1 0
     ^-X in slice-^  ^-----Y on trimmed CCD-----^ ^--IFU---^ ^--slice---^
   (the highest bit stays empty)@endverbatim
 *
 * The layout of these bits can change any time, so software using this should
 * use the accessor functions @c muse_pixtable_origin_get_*() to decode required
 * values.
 *
 * @note Several of the header keywords need to be changed when the data changes
 *       or parts of the table are erased!
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/* Let's use macros for the shift lengths we need */
#define MUSE_ORIGIN_SHIFT_XSLICE 24
#define MUSE_ORIGIN_SHIFT_YPIX   11
#define MUSE_ORIGIN_SHIFT_IFU     6
/* spacing between the left slice edge at the bottom of the slice      *
 * to the origin of the slice from where we could x positions within   *
 * the slice; this is to make sure that no pixel is left of the offset */
#define MUSE_ORIGIN_SLICE_SAFETY_OFFSET -20

/* private functions without error checking */
/** @private */
static inline uint32_t
muse_pixtable_origin_encode_fast(unsigned int aX, unsigned int aY,
                                 unsigned short aIFU,
                                 unsigned short aSlice, unsigned int aOffset)
{
  return ((aX - aOffset) << MUSE_ORIGIN_SHIFT_XSLICE)
         | (aY  << MUSE_ORIGIN_SHIFT_YPIX)
         | (aIFU << MUSE_ORIGIN_SHIFT_IFU)
         | aSlice;
}

/** @private */
static inline unsigned int
muse_pixtable_origin_get_x_fast(uint32_t aOrigin, uint32_t aOffset)
{
  return ((aOrigin >> MUSE_ORIGIN_SHIFT_XSLICE) & 0x7f) + aOffset;
}

/** @private */
static inline unsigned int
muse_pixtable_origin_get_y_fast(uint32_t aOrigin)
{
  return (aOrigin >> MUSE_ORIGIN_SHIFT_YPIX) & 0x1fff;
}

/** @private */
static inline unsigned short
muse_pixtable_origin_get_ifu_fast(uint32_t aOrigin)
{
  return (aOrigin >> MUSE_ORIGIN_SHIFT_IFU) & 0x1f;
}

/** @private */
static inline unsigned short
muse_pixtable_origin_get_slice_fast(uint32_t aOrigin)
{
  return aOrigin & 0x3f;
}

/*---------------------------------------------------------------------------*/
/**
  @brief   Encode the three CCD coordinates defining the origin of one MUSE
           pixel into a 32bit integer.
  @param   aX        The horizontal coordinate of the origin.
  @param   aY        The vertical coordinate of the origin.
  @param   aIFU      The IFU number where this pixel originated.
  @param   aSlice    The slice number where this pixel originated.
  @param   aOffset   The offset of the slice on the CCD.
  @return  The encoded (32bit) unsigned integer or 0 on error.

  aX and aY and aOffset have to be above 0 and below 8192; valid aIFU numbers
  are between 1 and 24, valid slice numbers between 1 and 48.
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return 0,
         aX\, aY\, aIFU\, aSlice\, or aOffset are out of range}
 */
/*---------------------------------------------------------------------------*/
uint32_t
muse_pixtable_origin_encode(unsigned int aX, unsigned int aY,
                            unsigned short aIFU,
                            unsigned short aSlice, unsigned int aOffset)
  /* use the explicit 32bit return type so that   *
   * this also works on systems with other setups */
{
  /* check for allowed values to fit into 32bit */
  cpl_ensure(aX < 8192 && aX > 0 && aY < 8192 && aY > 0 &&
             aIFU <= kMuseNumIFUs && aIFU >= 1 &&
             aSlice <= kMuseSlicesPerCCD && aSlice >= 1 && aOffset < 8192,
             CPL_ERROR_ILLEGAL_INPUT, 0);

#if 0
  cpl_msg_debug(__func__, "origin (%d, %d, %d, %d, %d) = 0x%x",
                aX, aY, aIFU, aSlice, aOffset,
                ((aX - aOffset) << MUSE_ORIGIN_SHIFT_XSLICE)
                | (aY << MUSE_ORIGIN_SHIFT_YPIX)
                | (aIFU << MUSE_ORIGIN_SHIFT_IFU)
                | aSlice);
#endif

  /* do the bit-shifting "magic" */
  return muse_pixtable_origin_encode_fast(aX, aY, aIFU, aSlice, aOffset);
} /* muse_pixtable_origin_encode() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the horizontal coordinate from the encoded 32bit origin number.
  @param   aOrigin     The encoded (32bit) unsigned integer.
  @param   aPixtable   The pixel table itself, the header component is
                       needed to find the slice offset.
  @param   aRow        The row index (starting at zero).
  @return  The decoded x coordinate or 0 on error.

  The row index is needed because the horizontal position on a CCD is measured
  within each slice and the offset of each slice may depend on the exposure
  number and the IFU. If the row index is unknown or does not matter (because
  the pixel table only contains one exposure of one IFU), pass a negative
  number.

  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         decoded horizontal coordinate value is out of range}
 */
/*---------------------------------------------------------------------------*/
unsigned int
muse_pixtable_origin_get_x(uint32_t aOrigin, muse_pixtable *aPixtable,
                           cpl_size aRow)
{
  unsigned short slice = muse_pixtable_origin_get_slice_fast(aOrigin),
                 ifu = muse_pixtable_origin_get_ifu_fast(aOrigin);
  cpl_errorstate prestate = cpl_errorstate_get();
  unsigned int expnum = muse_pixtable_get_expnum(aPixtable, aRow);
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_errorstate_set(prestate);
  }
  unsigned int offset = muse_pixtable_origin_get_offset(aPixtable, expnum, ifu,
                                                        slice),
               x = muse_pixtable_origin_get_x_fast(aOrigin, offset);
#if 0
  if (x > 8191 || x < 1 || !cpl_errorstate_is_equal(prestate)) {
    cpl_msg_error(__func__, "aOrigin=%#x x=%d (%d %d %d), %s",
                  aOrigin, x, slice, ifu, offset, cpl_error_get_message());
  }
#endif
  cpl_ensure(x <= 8191 && x >= 1 && cpl_errorstate_is_equal(prestate),
             CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return x;
} /* muse_pixtable_origin_get_x() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the vertical coordinate from the encoded 32bit origin number.
  @param   aOrigin   The encoded (32bit) integer.
  @return  The decoded y coordinate or 0 on error.
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         decoded vertical coordinate value is out of range}
 */
/*---------------------------------------------------------------------------*/
unsigned int
muse_pixtable_origin_get_y(uint32_t aOrigin)
{
  unsigned int y = muse_pixtable_origin_get_y_fast(aOrigin);
#if 0
  if (y > 8191 || y < 1) {
    cpl_msg_error(__func__, "aOrigin=%#x y=%d", aOrigin, y);
  }
#endif
  cpl_ensure(y <= 8191 && y >= 1, CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return y;
} /* muse_pixtable_origin_get_y() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the IFU number from the encoded 32bit origin number.
  @param   aOrigin   The encoded (32bit) integer.
  @return  The decoded IFU number or 0 on error.
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         decoded IFU number is out of range}
 */
/*---------------------------------------------------------------------------*/
unsigned short
muse_pixtable_origin_get_ifu(uint32_t aOrigin)
{
  unsigned short ifu = muse_pixtable_origin_get_ifu_fast(aOrigin);
#if 0
  if (ifu > kMuseNumIFUs || ifu < 1) {
    cpl_msg_error(__func__, "aOrigin=%#x ifu=%d", aOrigin, ifu);
  }
#endif
  cpl_ensure(ifu <= kMuseNumIFUs && ifu >= 1, CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return ifu;
} /* muse_pixtable_origin_get_ifu() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the slice number from the encoded 32bit origin number.
  @param   aOrigin   The encoded (32bit) integer.
  @return  The decoded slice number or 0 on error.
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         decoded slice number is out of range}
 */
/*---------------------------------------------------------------------------*/
unsigned short
muse_pixtable_origin_get_slice(uint32_t aOrigin)
{
  unsigned short slice = muse_pixtable_origin_get_slice_fast(aOrigin);
#if 0
  if (slice > kMuseSlicesPerCCD || slice < 1) {
    cpl_msg_error(__func__, "aOrigin=%#x slice=%d", aOrigin, slice);
  }
#endif
  cpl_ensure(slice <= kMuseSlicesPerCCD && slice >= 1,
             CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return slice;
} /* muse_pixtable_origin_get_slice() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Set the slice offset from the pixel table header.
  @param   aPixtable   The pixel table.
  @param   aLTrace     The tracing polynomial for the left edge of the slice.
  @param   aIFU        The relevant IFU number.
  @param   aSlice      The relevant slice number.
  @return  The computed slice offset or 0 on error.

  This function computes the offset for a given slice of a given IFU, using the
  trace function, and stores it in the FITS header of the pixel table.
  @error{set CPL_ERROR_NULL_INPUT\, return 0, pixel table or its header are NULL}
  @error{propagate CPL error\, return 0,
         trace polynomial could not be evalutated}
 */
/*---------------------------------------------------------------------------*/
unsigned int
muse_pixtable_origin_set_offset(muse_pixtable *aPixtable,
                                cpl_polynomial *aLTrace,
                                unsigned short aIFU, unsigned short aSlice)
{
  cpl_ensure(aPixtable && aPixtable->header, CPL_ERROR_NULL_INPUT, 0);
  cpl_errorstate prestate = cpl_errorstate_get();
  unsigned int offset = floor(cpl_polynomial_eval_1d(aLTrace, 1, NULL))
                      + MUSE_ORIGIN_SLICE_SAFETY_OFFSET;
  cpl_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(), 0);
  /* use exposure number 0 here, this is set only later, *
   * when merging pixel tables of different exposures    */
  char *keyword = cpl_sprintf(MUSE_HDR_PT_IFU_SLICE_OFFSET, 0, aIFU, aSlice);
  cpl_propertylist_update_int(aPixtable->header, keyword, offset);
  cpl_propertylist_set_comment(aPixtable->header, keyword,
                               MUSE_HDR_PT_IFU_SLICE_OFFSET_COMMENT);
  cpl_free(keyword);
  return offset;
} /* muse_pixtable_origin_set_offset() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the slice offset from the pixel table header.
  @param   aPixtable   The pixel table.
  @param   aExpNum     The relevant exposure number.
  @param   aIFU        The relevant IFU number.
  @param   aSlice      The relevant slice number.
  @return  The decoded slice offset or 0 on error.

  If the exposure number aExpNum is unknown, pass 0.

  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         decoded slice offset is out of range or header keyword missing for this combination of aExpNum/aIFU/aSlice}
 */
/*---------------------------------------------------------------------------*/
unsigned int
muse_pixtable_origin_get_offset(muse_pixtable *aPixtable, unsigned int aExpNum,
                                unsigned short aIFU, unsigned short aSlice)
{
  cpl_ensure(aPixtable && aPixtable->header, CPL_ERROR_NULL_INPUT, 0);
  char *keyword = cpl_sprintf(MUSE_HDR_PT_IFU_SLICE_OFFSET, aExpNum, aIFU,
                              aSlice);
  cpl_errorstate prestate = cpl_errorstate_get();
  unsigned int offset = cpl_propertylist_get_int(aPixtable->header, keyword);
  cpl_free(keyword);
#if 0
  if (offset > 8191 || offset < 1) {
    cpl_msg_error(__func__, "aIFU=%d aSlice=%d offset=%d",
                  aIFU, aSlice, offset);
  }
#endif
  cpl_ensure(offset <= 8191 && offset >= 1 && cpl_errorstate_is_equal(prestate),
             CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return offset;
} /* muse_pixtable_origin_get_offset() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Copy MUSE_HDR_PT_IFU_SLICE_OFFSET keywords between pixel tables.
  @param   aOut    output MUSE pixel table (can be input pixel table)
  @param   aFrom   optional input MUSE pixel table (can be NULL)
  @param   aNum    the exposure number to set in place of 0

  This can be used to set the exposure number of MUSE_HDR_PT_IFU_SLICE_OFFSET
  header keywords or to copy them around between different pixel tables with the
  exposure number staying zero.

  Loop through all possible IFU and slice numbers, copy the FITS keyword and
  value and delete the old one.

  If aFrom is NULL, then all keywords are copied/moved within the one respective
  property list. If aFrom is a valid pixel table, the output entries are copied
  from there, with the exposure number corrected.

  @error{return CPL_ERROR_NULL_INPUT, aOut or its header component are NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_origin_copy_offsets(muse_pixtable *aOut, muse_pixtable *aFrom,
                                  unsigned int aNum)
{
  cpl_ensure_code(aOut && aOut->header, CPL_ERROR_NULL_INPUT);
  cpl_propertylist *dest = aOut->header,
                   *from = aOut->header;
  if (aFrom && aFrom->header) {
    from = aFrom->header;
  }
  char keyword[KEYWORD_LENGTH];
  unsigned short nifu, nslice;
  for (nifu = 1; nifu <= kMuseNumIFUs; nifu++) {
    for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
      /* keyword to copy */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_IFU_SLICE_OFFSET,
               0, nifu, nslice);
      cpl_errorstate prestate = cpl_errorstate_get();
      unsigned int offset = cpl_propertylist_get_int(from, keyword);
      if (!cpl_errorstate_is_equal(prestate)) {
        /* this one was not set, skip to the next one */
        cpl_errorstate_set(prestate);
        continue;
      }
      /* erase old keyword */
      cpl_propertylist_erase(from, keyword);
      /* add new keyword */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_IFU_SLICE_OFFSET,
               aNum, nifu, nslice);
      cpl_propertylist_update_int(dest, keyword, offset);
      cpl_propertylist_set_comment(dest, keyword,
                                   MUSE_HDR_PT_IFU_SLICE_OFFSET_COMMENT);
    } /* for nslice */
  } /* for nifu */

  return CPL_ERROR_NONE;
} /* muse_pixtable_origin_copy_offsets() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Fill arrays with decoded entries from the origin column.
  @param   aPixtable   The pixel table to work on.
  @param   aX          pointer to output C array for the x CCD coordinate
  @param   aY          (optional) pointer to output C array for the y CCD
                       coordinate
  @param   aIFU        pointer to output C array for the IFU number
  @param   aSlice      pointer to output C array for the slice number
  @return  CPL_ERROR_NONE on success or another CPL error code on failure.

  To save memory, this function is implemented using bare C arrays of the
  smallest possible type, instead of using safer CPL arrays.
  The allocated arrays *aX, *aIFU, *aSlice, and possibly *aY need to be
  deallocated after use with cpl_free().

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{set and return CPL_ERROR_BAD_FILE_FORMAT,
         the table of aPixtable does not contains a MUSE_PIXTABLE_ORIGIN column}
  @error{set and return CPL_ERROR_NULL_INPUT,
         one of aX\, aIFU\, or aSlice is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_origin_decode_all(muse_pixtable *aPixtable,
                                unsigned short **aX, unsigned short **aY,
                                unsigned char **aIFU, unsigned char **aSlice)
{
  /* pixel table with origin column is needed */
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);
  const uint32_t *origin
    = (uint32_t *)cpl_table_get_data_int_const(aPixtable->table,
                                               MUSE_PIXTABLE_ORIGIN);
  cpl_ensure_code(origin, CPL_ERROR_BAD_FILE_FORMAT);
  cpl_ensure_code(aX && aIFU && aSlice, CPL_ERROR_NULL_INPUT);

  /* allocate the output buffers */
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  *aX = cpl_malloc(nrow * sizeof(unsigned short));
  if (aY) {
    *aY = cpl_malloc(nrow * sizeof(unsigned short));
  }
  *aIFU = cpl_malloc(nrow * sizeof(unsigned char));
  *aSlice = cpl_malloc(nrow * sizeof(unsigned char));

  /* check how many exposures there are in this pixel table: *
   * if there is only one (exposure number for first and last entry are
   * identical, i.e. zero), then we do not need to recompute the exposure number
   * at all, otherwise we need to do that for each new slice */
  cpl_errorstate prestate = cpl_errorstate_get();
  unsigned int expnum = muse_pixtable_get_expnum(aPixtable, 0),
               expnumN = muse_pixtable_get_expnum(aPixtable, nrow - 1);
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_errorstate_set(prestate);
  }
  cpl_boolean multiexp = expnum != expnumN;

  /* how loop through the table and fill the buffers */
  unsigned int offset = 0;
  cpl_size i;
  for (i = 0; i < nrow; i++) {
    (*aIFU)[i] = muse_pixtable_origin_get_ifu_fast(origin[i]);
    (*aSlice)[i] = muse_pixtable_origin_get_slice_fast(origin[i]);

    /* need to get the slice offset to get a proper X coordinate */
    if ((i == 0) || (i > 0 && ((*aSlice)[i] != (*aSlice)[i-1]))) {
      /* IFUs/exposures only change at slice boundaries */
      if (multiexp && i > 0 && (*aIFU)[i] != (*aIFU)[i-1]) {
        expnum = muse_pixtable_get_expnum(aPixtable, i);
      }

      /* need to (re-)compute the slice offset */
      offset = muse_pixtable_origin_get_offset(aPixtable, expnum, (*aIFU)[i],
                                               (*aSlice)[i]);
    } /* if new slice */

    (*aX)[i] = muse_pixtable_origin_get_x_fast(origin[i], offset);
    if (aY) {
      (*aY)[i]= muse_pixtable_origin_get_y_fast(origin[i]);
    }
  } /* for i (pixel table rows) */

  return CPL_ERROR_NONE;
} /* muse_pixtable_origin_decode_all() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the exposure number of a given row in a pixel table.
  @param   aPixtable   The pixel table to query.
  @param   aRow        The row index (starting at zero).
  @return  The exposure number, or 0 on error or if no exposure info was
           present in the pixel table.

  @error{set CPL_ERROR_NULL_INPUT\, return 0,
         input pixel table or its header component are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return 0,
         the row index is negative or beyond the table length}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0,
         output exposure number does not agree with low/high boundaries}
 */
/*---------------------------------------------------------------------------*/
unsigned int
muse_pixtable_get_expnum(muse_pixtable *aPixtable, cpl_size aRow)
{
  cpl_ensure(aPixtable && aPixtable->header, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure(aRow >= 0 && muse_pixtable_get_nrow(aPixtable) > aRow,
             CPL_ERROR_ILLEGAL_INPUT, 0);

  char keyword[KEYWORD_LENGTH];
  unsigned int exposure = 0;
  cpl_size lo = 0, hi = 0;
  do {
    cpl_errorstate prestate = cpl_errorstate_get();
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_FST, ++exposure);
    lo = cpl_propertylist_get_long_long(aPixtable->header, keyword);
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_LST, exposure);
    hi = cpl_propertylist_get_long_long(aPixtable->header, keyword);
    if (!cpl_errorstate_is_equal(prestate)) {
      if (exposure == 1) {
        /* no exposure headers in the table at all, so it's either the *
         * only one or the table was sorted, destroying exposure info  */
        lo = hi = aRow;
        exposure = 0; /* signify the problem to the caller */
      }
      cpl_errorstate_set(prestate);
      break;
    }
  } while (hi < aRow);
  cpl_ensure(lo <= aRow && hi >= aRow, CPL_ERROR_ILLEGAL_OUTPUT, 0);
  return exposure;
} /* muse_pixtable_get_expnum */

/*----------------------------------------------------------------------------*/
/**
 * @brief MUSE pixel table definition.
 *
 * The MUSE pixel table has the following columns:
 *
 * - 'xpos': the relative x-pixel position in the output datacube
 * - 'ypos': the relative y-pixel position in the output datacube
 * - 'lambda': wavelength of this pixel
 * - 'data': the data value in this pixel,
 *           this is in count (= electrons) units at time of table creation
 * - 'dq': the Euro3D bad pixel status of this pixel
 * - 'stat': the data variance of this pixel,
 *           in count**2 (= electrons**2) units at time of table creation
 * - 'origin': encoded value of IFU and slice number, as well as x and y
 *             position in the raw (trimmed) data
 *
 * It may additionally contain a column 'weight' which per-pixel weight
 * information to be used for resampling.
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_pixtable_def[] = {
  { MUSE_PIXTABLE_XPOS, CPL_TYPE_FLOAT, "pix", "%7.2f",
    "relative x-pixel position in the output datacube", CPL_TRUE},
  { MUSE_PIXTABLE_YPOS, CPL_TYPE_FLOAT, "pix", "%7.2f",
    "relative y-pixel position in the output datacube", CPL_TRUE},
  { MUSE_PIXTABLE_LAMBDA, CPL_TYPE_FLOAT, "Angstrom", "%8.2f",
    "wavelength of this pixel", CPL_TRUE},
  { MUSE_PIXTABLE_DATA, CPL_TYPE_FLOAT, "count", "%e",
    "data value in this pixel", CPL_TRUE},
  { MUSE_PIXTABLE_DQ, CPL_TYPE_INT, NULL, "%#x",
    "Euro3D bad pixel status of this pixel", CPL_TRUE},
  { MUSE_PIXTABLE_STAT, CPL_TYPE_FLOAT, "count**2", "%e",
    "data variance of this pixel", CPL_TRUE},
  { MUSE_PIXTABLE_ORIGIN, CPL_TYPE_INT, NULL, "0x%08x",
    "encoded value of IFU and slice number, as well as x and y "
    "position in the raw (trimmed) data", CPL_TRUE},
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*---------------------------------------------------------------------------*/
/**
  @brief   Create the pixel table for one CCD.
  @param   aImage      muse_image to be transformed to a pixel table
  @param   aTrace      the table containing the trace solution
  @param   aWave       the table containing the wavelength calibration solution
  @param   aGeoTable   MUSE geometry table containing at least the IFU in
                       question (optional)
  @return  the pixel table as muse_pixtable * or NULL in case of error.
  @remark The returned object has to be deallocated using muse_pixtable_delete().
  @remark If aGeoTable is NULL, simple slice locations are used, such that slices
          are only horizontally offset but are assumed to have no vertical offset
          and no rotation. This is good if you plan to resample the pixel table
          to 2D image (spatial and wavelength axes) afterwards instead of a 3D
          cube (2 spatial, 1 wavelength axis).

  Create the output table and table columns. Loop through all pixels in the
  input image. Evaluate, if the pixel is within a slice. If so, evaluate the
  respective wavelength solution, and the trace, and set the coordinates
  accordingly. Then set the pixel values (data, dq, and stat) to the values in
  the input image.

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

  @qa If the input calibration tables are correct, this function will give
      correct results. The only non-trivial parts are already tested in
      muse_wave_map() where errors are easily seen in the output file.

  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         the IFU number found in the input image header is not valid}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         no BUNIT is given in the header of aImage\, or it is not "count"}
  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         the input image\, the input wavelength calibration solution table\, or the input trace solution table are NULL}
  @error{use simple slice locations, the input geometry table is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         geometry table is given but unusable (missing IFU or missing x or y columns)}
  @error{set CPL_ERROR_BAD_FILE_FORMAT\, and either continue with defaults (MUSE_EXPERT_USER) or return NULL,
         geometry table does not contain angle or width columns}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, output error message\, and either continue (MUSE_EXPERT_USER)\, skipping the related slice\, or return NULL,
         the wavelength calibration table is invalid for a slice}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, output error message\, and either continue (MUSE_EXPERT_USER)\, skipping the related slice\, or return NULL,
         the trace table is invalid for a slice}
  @error{skip rest of that slice\, output warning,
         trace table gives invalid results for one vertical position of a slice}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_create(muse_image *aImage, cpl_table *aTrace, cpl_table *aWave,
                     cpl_table *aGeoTable)
{
  cpl_ensure(aImage && aWave && aTrace, CPL_ERROR_NULL_INPUT, NULL);
  /* get and check IFU number */
  unsigned char ifu = muse_utils_get_ifu(aImage->header);
  cpl_ensure(ifu >= 1 && ifu <= kMuseNumIFUs, CPL_ERROR_DATA_NOT_FOUND, NULL);

  /* check that the input data is in count units */
  if (!cpl_propertylist_has(aImage->header, "BUNIT")) {
    char *msg = cpl_sprintf("Input data of IFU %hhu does not contain a data "
                            "unit (\"BUNIT\"), cannot proceed!", ifu);
    cpl_msg_error(__func__, "%s", msg);
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "%s", msg);
    cpl_free(msg);
    return NULL;;
  }
  const char *unit = muse_pfits_get_bunit(aImage->header);
  if (strncmp(unit, "count", 6)) {
    char *msg = cpl_sprintf("Input data of IFU %hhu is not in \"count\" units "
                            "but in \"%s\", not suitable for a new pixel table!",
                            ifu, unit);
    cpl_msg_error(__func__, "%s", msg);
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "%s", msg);
    cpl_free(msg);
    return NULL;
  }

  /* create the output pixel table structure, and NULL out the components */
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));
  /* compute initial table size */
  int xsize = cpl_image_get_size_x(aImage->data),
      ysize = cpl_image_get_size_y(aImage->data),
#if PIXTABLE_CREATE_CCDSIZED
      /* There cannot be more pixels needed in the table than in the       *
       * input image, so with this we can remove the overflow check below. */
      isize = xsize * ysize;
#else
      /* It's very likely that no more pixels than these are needed. But then *
       * we need to check for overflows below (and still cut it in the end).  */
      isize = ysize * kMuseSlicesPerCCD * kMuseSliceHiLikelyWidth;
#endif
  pt->table = muse_cpltable_new(muse_pixtable_def, isize);

  /* copy the header from the input image to the pixel table, *
   * but remove some keywords that could cause trouble        */
  pt->header = cpl_propertylist_duplicate(aImage->header);
  cpl_propertylist_erase_regexp(pt->header,
                                "^SIMPLE$|^BITPIX$|^NAXIS|^EXTEND$|^XTENSION$|"
                                "^DATASUM$|^DATAMIN$|^DATAMAX$|^DATAMD5$|"
                                "^PCOUNT$|^GCOUNT$|^HDUVERS$|^BLANK$|"
                                "^BZERO$|^BSCALE$|^BUNIT$|^CHECKSUM$|^INHERIT$|"
                                MUSE_HDR_OVSC_REGEXP"|"MUSE_WCS_KEYS, 0);
  cpl_propertylist_append_string(pt->header, MUSE_HDR_PT_TYPE,
                                 aGeoTable ? MUSE_PIXTABLE_STRING_FULL
                                           : MUSE_PIXTABLE_STRING_SIMPLE);
  cpl_propertylist_set_comment(pt->header, MUSE_HDR_PT_TYPE,
                               aGeoTable ? MUSE_PIXTABLE_COMMENT_FULL
                                         : MUSE_PIXTABLE_COMMENT_SIMPLE);

  /* make all table cells valid by filling them with zeros; this is a workaround *
   * suggested by Carlo Izzo to be able to set the table cells using array       *
   * access instead of the slower and non-threadsafe cpl_table_set() function    */
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_XPOS, 0, isize, 0);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_YPOS, 0, isize, 0);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_LAMBDA, 0, isize, 0);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_DATA, 0, isize, 0);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_STAT, 0, isize, 0);
  cpl_table_fill_column_window_int(pt->table, MUSE_PIXTABLE_DQ, 0, isize, 0);
  cpl_table_fill_column_window_int(pt->table, MUSE_PIXTABLE_ORIGIN, 0, isize, 0);

  /* get columns as pointers */
  float *cdata_xpos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_XPOS),
        *cdata_ypos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_YPOS),
        *cdata_lambda = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_LAMBDA),
        *cdata_data = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_DATA),
        *cdata_stat = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_STAT);
  int *cdata_dq = cpl_table_get_data_int(pt->table, MUSE_PIXTABLE_DQ);
  uint32_t *cdata_origin = (uint32_t *)cpl_table_get_data_int(pt->table,
                                                              MUSE_PIXTABLE_ORIGIN);

  /* get input image buffers as pointers, too */
  const float *pixdata = cpl_image_get_data_float_const(aImage->data),
              *pixstat = cpl_image_get_data_float_const(aImage->stat);
  const int *pixdq = cpl_image_get_data_int_const(aImage->dq);

  /* get columns of the wavecal table */
  unsigned short wavexorder, waveyorder;
  muse_wave_table_get_orders(aWave, &wavexorder, &waveyorder);
  cpl_msg_info(__func__, "Creating pixel table for IFU %hhu, using order %d for "
               "trace solution and orders %hu/%hu for wavelength solution", ifu,
               muse_trace_table_get_order(aTrace), wavexorder, waveyorder);

  /* get position corresponding to the IFU number */
  cpl_table *geopos = NULL;
  double *slice_x = NULL, *slice_y = NULL, *slice_angle = NULL,
         *slice_width = NULL;
  if (aGeoTable) {
    geopos = muse_geo_table_extract_ifu(aGeoTable, ifu);
    slice_x = cpl_table_get_data_double(geopos, MUSE_GEOTABLE_X);
    slice_y = cpl_table_get_data_double(geopos, MUSE_GEOTABLE_Y);
    /* fail on missing x/y as they are critical */
    if (!geopos || !slice_x || !slice_y) {
      char *msg = !geopos
                ? cpl_sprintf("Geometry table is missing data for IFU %hhu!", ifu)
                : cpl_sprintf("Geometry table is missing column%s%s%s!",
                              !slice_x && !slice_y ? "s" : "",
                              slice_x ? "" : " \""MUSE_GEOTABLE_X"\"",
                              slice_y ? "" : " \""MUSE_GEOTABLE_Y"\"");
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "%s", msg);
      cpl_msg_error(__func__, "%s", msg);
      cpl_free(msg);
      cpl_table_delete(geopos);
      muse_pixtable_delete(pt);
      return NULL;
    }
    /* only output warnings for other missing columns, they *
     * default to usable values below, ignore CPL errors    */
    cpl_errorstate prestate = cpl_errorstate_get();
    slice_angle = cpl_table_get_data_double(geopos, MUSE_GEOTABLE_ANGLE);
    slice_width = cpl_table_get_data_double(geopos, MUSE_GEOTABLE_WIDTH);
    if (!cpl_errorstate_is_equal(prestate)) {
      char *msg = cpl_sprintf("Geometry table is missing column%s%s%s!",
                              !slice_angle && !slice_width ? "s" : "",
                              slice_angle ? "" : " \""MUSE_GEOTABLE_ANGLE"\"",
                              slice_width ? "" : " \""MUSE_GEOTABLE_WIDTH"\"");
      cpl_error_set_message(__func__, CPL_ERROR_BAD_FILE_FORMAT, "%s", msg);
      cpl_msg_error(__func__, "%s", msg);
      cpl_free(msg);
      if (!getenv("MUSE_EXPERT_USER")) {
        cpl_table_delete(geopos);
        muse_pixtable_delete(pt);
        return NULL;
      }
    } /* if */
  } /* if aGeoTable */

  /* this counts the current table row that is being entered */
  cpl_size itablerow = 0;
  /* time the operation for this IFU */
  double cputime = cpl_test_get_cputime(),
         walltime = cpl_test_get_walltime();
  /* loop through all slices */
  int islice;
#if DEBUG_PIXTABLE_FEW_SLICES > 0
# define SLICE_LIMIT   DEBUG_PIXTABLE_FEW_SLICES
#else
# define SLICE_LIMIT   kMuseSlicesPerCCD
#endif
  for (islice = 0; islice < SLICE_LIMIT; islice++) {
#if DEBUG_PIXTABLE_CREATION /* check how many pixels extend over the nominal width */
    cpl_msg_debug(__func__, "Starting to process slice %2d of IFU %2hhu",
                  islice + 1, ifu);
#endif

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

    /* test evaluate the wavelength polynomial to see if it's valid */
    cpl_vector_set(pos, 0, kMuseOutputYTop / 2);
    cpl_vector_set(pos, 1, kMuseOutputYTop / 2);
    double ltest = cpl_polynomial_eval(pwave, pos);
    if (!pwave || !isnormal(ltest) || fabs(ltest) < DBL_EPSILON) {
      char *msg = cpl_sprintf("Wavelength calibration polynomial for slice %d "
                              "of IFU %hhu is not well defined!", islice + 1,
                              ifu);
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "%s", msg);
      cpl_msg_error(__func__, "%s", msg);
      cpl_free(msg);
      cpl_polynomial_delete(pwave);
      cpl_vector_delete(pos);
      if (getenv("MUSE_EXPERT_USER")) {
        continue; /* go the next slice */
      } else { /* give up completely */
        cpl_table_delete(geopos);
        muse_pixtable_delete(pt);
        return NULL;
      }
    } /* if bad wavecal polynomial */

    /* get the tracing polynomials for this slice */
    cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
                                                                   islice + 1);
    if (!ptrace) {
      char *msg = cpl_sprintf("Tracing polynomials for slice %d of IFU %hhu are"
                              " not well defined!", islice + 1, ifu);
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "%s", msg);
      cpl_msg_error(__func__, "%s", msg);
      cpl_free(msg);
      cpl_polynomial_delete(pwave);
      cpl_vector_delete(pos);
      if (getenv("MUSE_EXPERT_USER")) {
        continue; /* go the next slice */
      } else { /* give up completely */
        cpl_table_delete(geopos);
        muse_pixtable_delete(pt);
        return NULL;
      }
    } /* if no tracing polynomials */
    unsigned offset = muse_pixtable_origin_set_offset(pt,
                                                      ptrace[MUSE_TRACE_LEFT],
                                                      ifu, islice + 1);

    /* within each slice, loop from bottom to top */
    int j;
    for (j = 1; j <= ysize; j++) {
      /* compute slice center for this vertical position and set the *
       * edge pixels of slice, so that we can loop over those pixels */
      double x1 = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], j, NULL),
             x2 = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], j, NULL),
             xcenter = (x1 + x2) / 2.,
             width = x2 - x1;
      if (!isnormal(x1) || !isnormal(x2) || x1 < 1 || x2 > xsize || x1 > x2) {
        cpl_msg_warning(__func__, "slice %2d of IFU %hhu: faulty polynomial "
                        "detected at y=%d (borders: %f ... %f)", islice + 1,
                        ifu, j, x1, x2);
        break; /* skip the rest of this slice */
      }
      /* include pixels that have more than half of them inside the slice */
      int ileft = ceil(x1),
          iright = floor(x2);
      cpl_vector_set(pos, 1, j); /* vertical pos. for wavelength evaluation */

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

        /* do the wavelength evaluation early on, to more quickly *
         * recover from errors                                    */
        double lambda = cpl_polynomial_eval(pwave, pos);
        if (lambda < 3000. || lambda > 11000. || !isfinite(lambda)) {
          continue; /* skip this pixel */
        }

        /* Relative horizontal pixel position within slice; has to be scaled *
         * by the width of the slice on the CCD.  Slice width is measured    *
         * horizontally, not along iso-lambda lines, the difference for      *
         * straight lines is below 0.015 pix (0.02%), and only slightly      *
         * larger for curved lines, so this simplification should not have a *
         * negative effect on anything.  We compute (xcenter - i) to account *
         * for the reversed sky orientation within each slice.               */
        float dx = (xcenter - i) / width
                 * (slice_width ? slice_width[islice] : kMuseSliceNominalWidth);
#if DEBUG_PIXTABLE_CREATION /* check how many pixels extend over the nominal width */
        if (fabs(dx) > 37.5) {
          cpl_msg_debug(__func__, "%d - %f -> %f", i, xcenter, dx);
        }
#endif

#if CREATE_MINIMAL_PIXTABLE
        /* if we need to create a very small pixel table for debugging */
        if (lambda < 6495. || lambda > 6505. || fabs(dx) > 10.) {
          continue;
        }
#endif

        /* this pixel is set up to be inside the slice, write it to the table */
        cpl_size irow = itablerow++; /* started at 0, so increment after assignment */
#if !PIXTABLE_CREATE_CCDSIZED /* overflow check, and possibly enlarge the table */
        if (irow + 1 > (cpl_size)muse_pixtable_get_nrow(pt)) {
#if DEBUG_PIXTABLE_CREATION /* make sure that we are filling the right part */
          printf("pixel table before enlargement:\n");
          cpl_table_dump(pt->table, irow-5, 10, stdout);
          fflush(stdout);
#endif
          /* If the table is too small, we can as well add a lot more rows     *
           * right now, than adding them in small portions. If we add too many *
           * this will only marginally decrease the speed of this procedure    */
          const cpl_size nr = 1000000;
          cpl_msg_debug(__func__, "expand table to %"CPL_SIZE_FORMAT" rows "
                        "(add %"CPL_SIZE_FORMAT" lines)!", irow + nr, nr);
          cpl_table_set_size(pt->table, irow + nr);

          /* again fill the new section of each column */
          cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_XPOS, irow, nr, 0);
          cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_YPOS, irow, nr, 0);
          cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_LAMBDA, irow, nr, 0);
          cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_DATA, irow, nr, 0);
          cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_STAT, irow, nr, 0);
          cpl_table_fill_column_window_int(pt->table, MUSE_PIXTABLE_DQ, irow, nr, 0);
          cpl_table_fill_column_window_int(pt->table, MUSE_PIXTABLE_ORIGIN, irow, nr, 0);

          /* re-get columns as pointers, the memory location might have changed */
          cdata_xpos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_XPOS);
          cdata_ypos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_YPOS);
          cdata_lambda = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_LAMBDA);
          cdata_data = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_DATA);
          cdata_stat = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_STAT);
          cdata_dq = cpl_table_get_data_int(pt->table, MUSE_PIXTABLE_DQ);
          cdata_origin = (uint32_t *)cpl_table_get_data_int(pt->table,
                                                            MUSE_PIXTABLE_ORIGIN);
#if DEBUG_PIXTABLE_CREATION /* make sure that we have filled the right part */
          printf("pixel table after filling new rows:\n");
          cpl_table_dump(pt->table, irow-5, 10, stdout);
          fflush(stdout);
#endif
        } /* if new rows needed */
#endif /* !PIXTABLE_CREATE_CCDSIZED */

        /* Assign the coordinates (slice angle is in degrees). */
        double angle = slice_angle ? slice_angle[islice] * CPL_MATH_RAD_DEG
                                   : 0.;
        cdata_xpos[irow] = aGeoTable
                         ? dx * cos(angle) + slice_x[islice]
                         : dx + islice * kMuseSliceHiLikelyWidth
                           + kMuseSliceHiLikelyWidth / 2.;
        cdata_ypos[irow] = aGeoTable
                         ? dx * sin(angle) + slice_y[islice]
                         : 0.; /* no vertical position for simple case */
#if DEBUG_PIXTABLE_CREATION
        if (!isfinite(cdata_xpos[irow]) ||
            cdata_xpos[irow] < -200 || cdata_xpos[irow] > 4000) {
          cpl_msg_debug(__func__, "weird data in x: %e %e",
                        cdata_xpos[irow], cdata_ypos[irow]);
        }
        if (!isfinite(cdata_ypos[irow]) ||
            cdata_ypos[irow] < -200 || cdata_ypos[irow] > 200) {
          cpl_msg_debug(__func__, "weird data in y: %e %e",
                        cdata_xpos[irow], cdata_ypos[irow]);
        }
#endif
        cdata_lambda[irow] = lambda;

        /* assign the data values */
        cdata_data[irow] = pixdata[(i-1) + (j-1)*xsize];
        cdata_dq[irow] = pixdq[(i-1) + (j-1)*xsize];
        cdata_stat[irow] = pixstat[(i-1) + (j-1)*xsize];
        /* assign origin and slice number */
        cdata_origin[irow] = muse_pixtable_origin_encode_fast(i, j, ifu,
                                                              islice + 1,
                                                              offset);
      } /* for i (horizontal pixels) */
    } /* for j (vertical direction) */

    /* we are now done with this slice, clean up */
    muse_trace_polys_delete(ptrace);
    cpl_polynomial_delete(pwave);
    cpl_vector_delete(pos);
  } /* for islice */
  cpl_table_delete(geopos);
  cpl_msg_debug(__func__, "IFU %hhu took %gs (CPU time), %gs (wall-clock)",
                ifu, cpl_test_get_cputime() - cputime,
                cpl_test_get_walltime() - walltime);

  /* itablerow is the last written to location (starting at zero), so add one */
  if (muse_pixtable_get_nrow(pt) > itablerow) {
    cpl_msg_debug(__func__, "Trimming pixel table of IFU %hhu to %"CPL_SIZE_FORMAT
                  " of %"CPL_SIZE_FORMAT" rows", ifu, itablerow,
                  muse_pixtable_get_nrow(pt));
#if DEBUG_PIXTABLE_CREATION /* make sure that we are trimming correctly */
    printf("end of used part of pixel table before trimming:\n");
    cpl_table_dump(pt->table, itablerow-5, 10, stdout);
    fflush(stdout);
#endif
    cpl_table_set_size(pt->table, itablerow);
  }
  /* now that we are done creating it, we can also *
   * compute its extremes for the first time       */
  muse_pixtable_compute_limits(pt);
#if DEBUG_PIXTABLE_CREATION
  printf("beginning of pixel table before returning:\n");
  cpl_table_dump(pt->table, 0, 5, stdout);
  fflush(stdout);
  printf("end of pixel table before returning:\n");
  cpl_table_dump(pt->table, muse_pixtable_get_nrow(pt)-5, 10, stdout);
  fflush(stdout);
#endif

  return pt;
} /* muse_pixtable_create() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Make a copy of the pixel table.
  @param   aPixtable   MUSE pixel table
  @return  Pointer to the new table, or NULL in case of NULL input

  The copy operation is done "in depth": column data are duplicated too, not
  just their pointers. Also the selection flags of the original table are
  transferred to the new table. The header of the pixel table are copied, too.

  @note Passing in a NULL argument causes no error to be raised, it just
        returns a NULL pointer.
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_duplicate(muse_pixtable *aPixtable)
{
  if (!aPixtable) {
    return NULL;
  }
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));
  pt->table = cpl_table_duplicate(aPixtable->table);
  pt->header = cpl_propertylist_duplicate(aPixtable->header);
  if (aPixtable->ffspec) {
    pt->ffspec = cpl_table_duplicate(aPixtable->ffspec);
  }
  return pt;
}

/*---------------------------------------------------------------------------*/
/**
  @brief   Deallocate memory associated to a pixel table object.
  @param   aPixtable   input MUSE pixel table

  Calls cpl_table_delete() and cpl_propertylist_delete() for the two components
  of a muse_pixtable, and frees memory for the aPixtable pointer. As a safeguard,
  it checks if a valid pointer was passed, so that crashes cannot occur.
 */
/*---------------------------------------------------------------------------*/
void
muse_pixtable_delete(muse_pixtable *aPixtable)
{
  /* if we already get NULL, we don't need to do anything */
  if (!aPixtable) {
    return;
  }

  /* delete the table portion */
  cpl_table_delete(aPixtable->table);
  aPixtable->table = NULL;

  /* delete the header portion */
  cpl_propertylist_delete(aPixtable->header);
  aPixtable->header = NULL;

  /* delete the flat-field spectrum */
  cpl_table_delete(aPixtable->ffspec);
  aPixtable->ffspec = NULL;

  cpl_free(aPixtable);
} /* muse_pixtable_delete() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Create flat-field spectrum and append to pixel table.
  @param   aPixtable   input MUSE pixel table
  @param   aFF         the MUSE image containing a master-flat field exposure
  @param   aTrace      the table containing the trace solution
  @param   aWave       the table containing the wavelength calibration solution
  @param   aSampling   the sampling for the flat-field spectrum [Angstrom/pix]
  @return CPL_ERROR_NONE on success or the relevant cpl_error_code on error

  This function creates a pixel table from the input image (aFF) and tables
  (aTrace and aWave) using @ref muse_pixtable_create(), and then resamples it
  to a spectrum table, using @ref muse_resampling_spectrum(). The variance and
  quality columns or this spectrum are then discarded, the columns of the table
  created here are hence @ref MUSE_PIXTABLE_FFLAMBDA and @ref
  MUSE_PIXTABLE_FFDATA. The table is attached to the input pixel table
  (aPixtable) in the ffspec component.

  Any table that is set in the ffspec component of the input pixel table before
  the call to this function is deleted.

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{propagate error code of muse_pixtable_create(),
         muse_pixtable_create() fails}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_append_ff(muse_pixtable *aPixtable, muse_image *aFF,
                        cpl_table *aTrace, cpl_table *aWave, float aSampling)
{
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);

  /* discard a pre-existing flat-field spectrum */
  if (aPixtable->ffspec) {
    cpl_table_delete(aPixtable->ffspec);
  }

  /* create pixel table, without geometry table is fine */
  muse_pixtable *ptflat = muse_pixtable_create(aFF, aTrace, aWave, NULL);
  if (!ptflat) {
    return cpl_error_get_code();
  }
  aPixtable->ffspec = muse_resampling_spectrum(ptflat, aSampling);
  muse_pixtable_delete(ptflat);
  /* there does not seem to be a way to generate a muse_resampling_spectrum() *
   * failure other than with a NULL pixel table, so do not test for that      */

  /* muse_resampling_spectrum() also creates variance and bad pixel     *
   * spectra in extra columns which we do not need to carry around here */
  cpl_table_erase_column(aPixtable->ffspec, "stat");
  cpl_table_erase_column(aPixtable->ffspec, "dq");

  return CPL_ERROR_NONE;
} /* muse_pixtable_append_ff() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief  Save the flat-field spectrum as new extension to a MUSE pixel table.
  @param  aPixtable   input MUSE pixel table
  @param  aFilename   name of the output file
  @return CPL_ERROR_NONE on success or the relevant cpl_error_code on error

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{propagate CPL error,
         saving of the flat-field spectrum table fails}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_pixtable_save_ffspec(muse_pixtable *aPixtable, const char *aFilename)
{
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);

  /* if the flat-field spectrum does not exist, return early */
  if (!aPixtable->ffspec) {
    /* but warn, if the flag was set in the primary header *
     * (too late now to do anything about it)              */
    return CPL_ERROR_NONE;
  }

  /* save the flat-field in a new file extension */
  cpl_propertylist *hext = cpl_propertylist_new();
  cpl_propertylist_append_string(hext, "EXTNAME", MUSE_PIXTABLE_FF_EXT);
  cpl_error_code rc = cpl_table_save(aPixtable->ffspec, NULL, hext, aFilename,
                                     CPL_IO_EXTEND);
  cpl_propertylist_delete(hext);

  return rc;
} /* muse_pixtable_save_ffspec() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief  Save a MUSE pixel table as multi-extension FITS image.
  @param  aPixtable   input MUSE pixel table
  @param  aFilename   name of the output file
  @return CPL_ERROR_NONE on success or the relevant cpl_error_code on error

  This re-casts all columns of the table component to be cpl_images, and saves
  them subsequently into extensions of the given filename. The primary header
  is saved separately, EXTNAMEs of all extensions are the same as the column
  names of the pixel table. The column units, if they are set, are saved in the
  BUNIT header of each extension.

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{set and return CPL_ERROR_ILLEGAL_INPUT, aPixtable has zero rows}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, continue with next column,
         table column is of unsupported type (neither int nor float)}
  @error{propagate CPL error,
         saving of the column data in an image extension fails}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_pixtable_save_image(muse_pixtable *aPixtable, const char *aFilename)
{
  const char *id = "muse_pixtable_save"; /* pretend to be in that function */
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  cpl_ensure_code(nrow > 0, CPL_ERROR_ILLEGAL_INPUT);

  cpl_errorstate state = cpl_errorstate_get();
  cpl_array *columns = cpl_table_get_column_names(aPixtable->table);
  int icol, ncol = cpl_array_get_size(columns);
  for (icol = 0; icol < ncol; icol++) {
    const char *colname = cpl_array_get_string(columns, icol);
    /* access data buffer and wrap as corresponding CPL image type */
    cpl_type type = cpl_table_get_column_type(aPixtable->table, colname);
    cpl_image *image = NULL;
    switch (type) {
    case CPL_TYPE_FLOAT: {
      float *data = cpl_table_get_data_float(aPixtable->table, colname);
      image = cpl_image_wrap_float(1, nrow, data);
      break;
      }
    case CPL_TYPE_INT: {
      int *data = cpl_table_get_data_int(aPixtable->table, colname);
      image = cpl_image_wrap_int(1, nrow, data);
      break;
      }
    default:
      cpl_error_set_message(id, CPL_ERROR_UNSUPPORTED_MODE, "type \"%s\" (of "
                            "column %s) is not supported for MUSE pixel tables",
                            cpl_type_get_name(type), colname);
      continue;
    } /* switch */
    /* now construct minimal extension header with EXTNAME and *
     * BUNIT and save the image with its internal data type    */
    cpl_propertylist *hext = cpl_propertylist_new();
    cpl_propertylist_append_string(hext, "EXTNAME", colname);
    const char *unit = cpl_table_get_column_unit(aPixtable->table, colname);
    if (unit) {
      cpl_propertylist_append_string(hext, "BUNIT", unit);
    }
    cpl_image_save(image, aFilename, CPL_TYPE_UNSPECIFIED, hext, CPL_IO_EXTEND);
    cpl_image_unwrap(image);
    cpl_propertylist_delete(hext);
  } /* for icol (all table columns) */
  cpl_array_delete(columns);

  /* append the flat-field spectrum to the file (if it exists) */
  muse_pixtable_save_ffspec(aPixtable, aFilename);

  return cpl_errorstate_is_equal(state) ? CPL_ERROR_NONE : cpl_error_get_code();
} /* muse_pixtable_save_image() */

/*---------------------------------------------------------------------------*/
/**
  @brief  Save a MUSE pixel table to a file on disk.
  @param  aPixtable   input MUSE pixel table
  @param  aFilename   name of the output file
  @return CPL_ERROR_NONE on success or the relevant cpl_error_code on error

  Simply calls cpl_propertylist_save() using the keywords in the header element
  of the muse_pixtable structure for construction of the primary header.

  If the environment variable MUSE_PIXTABLE_SAVE_AS_TABLE is set to a positive
  integer, the pixel table is saved as FITS binary table using
  cpl_table_save().
  Otherwise all columns are saved as separate FITS image extensions where the
  table column names are converted to EXTNAME of each extension and the column
  units are saved as BUNIT.

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{set error message\, propagate CPL error code, failure to save headers}
  @error{propagate CPL error code, failure to save table}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_save(muse_pixtable *aPixtable, const char *aFilename)
{
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);

  /* let CPL do the real work and propagate any errors and errorstates that *
   * it generates; the secondary header should be generated automatically   *
   * from the table data we have set in muse_pixtable_create().             */
  /* save headers separately to keep WCS information */
  cpl_error_code rc = cpl_propertylist_save(aPixtable->header, aFilename,
                                            CPL_IO_CREATE);
  if (rc != CPL_ERROR_NONE) {
    cpl_error_set_message(__func__, rc, "could not save FITS header of pixel "
                          "table \"%s\"", aFilename);
    return rc;
  }
  /* test if we need to save it as image */
  cpl_boolean astable = getenv("MUSE_PIXTABLE_SAVE_AS_TABLE")
                      && atoi(getenv("MUSE_PIXTABLE_SAVE_AS_TABLE")) > 0;
  if (astable) {
    cpl_msg_debug(__func__, "Saving pixel table \"%s\" as binary table",
                  aFilename);
    rc = cpl_table_save(aPixtable->table, NULL, NULL, aFilename, CPL_IO_EXTEND);
    /* append the flat-field spectrum to the file (if it exists) */
    muse_pixtable_save_ffspec(aPixtable, aFilename);
  } else {
    rc = muse_pixtable_save_image(aPixtable, aFilename);
  }
  return rc;
} /* muse_pixtable_save() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Load a range of rows from the multi-extension FITS image that
           contains a MUSE pixel table.
  @param   aFilename   name of the file to load
  @param   aStart      first table row index to read (starting at zero)
  @param   aNRows      maximum number of rows to load
  @return  a new cpl_table * or NULL on error

  This loads all image extensions of the given file, and re-casts them into
  columns of a cpl_table. The column names are taken from the EXTNAME of each
  extension, the column units are read from the BUNIT entry, if they exist. Only
  CPL_TYPE_FLOAT and CPL_TYPE_INT are supported as image/column types here.

  If a MUSE_PIXTABLE_FF_EXT extension exists, this table is read as well and
  sets the @ref muse_pixtable.ffspec component of the pixel table.

  @error{propagate CPL error\, continue with next extension,
         could not load an image from an extension}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, continue with next extension,
         one image extension has a different number of pixels than the first}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, continue with next extension,
         one image extension is of unsupported type (neither int nor float)}
 */
/*---------------------------------------------------------------------------*/
static cpl_table *
muse_pixtable_load_window_image(const char *aFilename,
                                cpl_size aStart, cpl_size aNRows)
{
  const char *id = "muse_pixtable_load"; /* pretend to be in that function */
  /* determine "image" window to read, the y-direction *
   * in the image is the dimension of the rows         */
  cpl_size y1 = aStart + 1,
           y2 = aStart + aNRows;
  /* get header of "data" extension, and check/correct the size of the dataset */
  int ext = cpl_fits_find_extension(aFilename, MUSE_PIXTABLE_DATA);
  cpl_propertylist *hdata = cpl_propertylist_load(aFilename, ext);
  /* get NAXIS2 which contains contains the number of rows */
  cpl_size naxis2 = muse_pfits_get_naxis(hdata, 2);
  if (y2 > naxis2) {
    y2 = naxis2;
  }
  cpl_propertylist_delete(hdata);

  /* find and load all image extensions using their EXTNAMEs */
  cpl_size nrow = 0;
  cpl_table *table = cpl_table_new(nrow);
  int iext, next = cpl_fits_count_extensions(aFilename);
  for (iext = 1; iext <= next; iext++) {
    /* first check, that we are not loading the flat-field extension */
    cpl_propertylist *hext = cpl_propertylist_load(aFilename, iext);
    const char *colname = muse_pfits_get_extname(hext);
    if (!strncmp(colname, MUSE_PIXTABLE_FF_EXT, strlen(MUSE_PIXTABLE_FF_EXT) + 1)) {
      /* this will be loaded later */
      cpl_propertylist_delete(hext);
      continue;
    }

    /* now load the column from the image extension */
    cpl_errorstate ps = cpl_errorstate_get();
    cpl_image *column = cpl_image_load_window(aFilename, CPL_TYPE_UNSPECIFIED,
                                              0, iext, 1, y1, 1, y2);
    if (!column || !cpl_errorstate_is_equal(ps)) {
      cpl_image_delete(column);
      cpl_error_set_message(id,  cpl_error_get_code(), "could not load extension"
                            " %d of pixel table \"%s\"", iext, aFilename);
      cpl_propertylist_delete(hext);
      continue;
    }
    cpl_size nrows = cpl_image_get_size_x(column)
                   * cpl_image_get_size_y(column);
    if (nrow < 1) {
      cpl_table_set_size(table, nrows);
      nrow = nrows;
    } else if (nrows != nrow) {
      cpl_error_set_message(id, CPL_ERROR_INCOMPATIBLE_INPUT, "size of column "
                            "%s does not match", colname);
      cpl_propertylist_delete(hext);
      cpl_image_delete(column);
      continue;
    }
    cpl_type type = cpl_image_get_type(column);
    switch (type) {
    case CPL_TYPE_FLOAT:
      cpl_table_wrap_float(table, cpl_image_unwrap(column), colname);
      break;
    case CPL_TYPE_INT:
      cpl_table_wrap_int(table, cpl_image_unwrap(column), colname);
      break;
    default:
      cpl_error_set_message(id, CPL_ERROR_UNSUPPORTED_MODE, "type \"%s\" (of "
                            "column %s) is not supported for MUSE pixel tables",
                            cpl_type_get_name(type), colname);
    } /* switch */
    cpl_errorstate state = cpl_errorstate_get();
    const char *unit = muse_pfits_get_bunit(hext);
    if (!cpl_errorstate_is_equal(state)) {
      cpl_errorstate_set(state);
    }
    if (unit) {
      cpl_table_set_column_unit(table, colname, unit);
    }
    cpl_propertylist_delete(hext);
  } /* for iext (all FITS extensions) */
  return table;
} /* muse_pixtable_load_window_image() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Load the flat-field spectrum that was saved with a pixel table.
  @param   aFilename   name of the file to load
  @param   aPixtable   the pixel table that already contains header and table
  @return  CPL_ERROR_NONE or another CPL error code on failure.

  @error{propagate CPL error\, continue with next extension,
         could not load an image from an extension}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, continue with next extension,
         one image extension has a different number of pixels than the first}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, continue with next extension,
         one image extension is of unsupported type (neither int nor float)}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_pixtable_load_ffspec(const char *aFilename, muse_pixtable *aPixtable)
{
  const char *id = "muse_pixtable_load"; /* pretend to be in that function */
  cpl_ensure_code(aFilename && aPixtable, CPL_ERROR_NULL_INPUT);

  int iext = cpl_fits_find_extension(aFilename, MUSE_PIXTABLE_FF_EXT);
  /* if the flat-field extension was not found, just quietly *
   * return -- or make some noise if the flag was set        */
  if (iext <= 0) {
    return CPL_ERROR_NONE; /* quietly return */
  }

  cpl_errorstate es = cpl_errorstate_get();
  aPixtable->ffspec = cpl_table_load(aFilename, iext, 1);
  if (!cpl_errorstate_is_equal(es)) {
    cpl_msg_warning(id, "Pixel table flat-field spectrum extension %s "
                    "exists in \"%s\", but cannot be loaded!",
                    MUSE_PIXTABLE_FF_EXT, aFilename);

    /* clean up again, in case something (broken?) was loaded */
    cpl_table_delete(aPixtable->ffspec);
    aPixtable->ffspec = NULL;
    /* swallow the error code, since the missing flat-field only becomes *
     * critical, if this pixel table is later combined with others       */
    cpl_errorstate_set(es);
  }

  return CPL_ERROR_NONE;
} /* muse_pixtable_load_ffspec() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Load a range of rows from the table and all the FITS headers of a
           MUSE pixel table from a file.
  @param   aFilename   name of the file to load
  @param   aStart      first table row index to read (starting at zero)
  @param   aNRows      maximum number of rows to load
  @return  a new muse_pixtable * or NULL on error
  @remark  The new table has to be deallocated using muse_pixtable_delete().

  The primary FITS header is loaded into the header element using
  cpl_propertylist_load(). Then cpl_table_load_window() is used to allocate and
  load the table elements of a muse_pixtable from the input FITS file.

  It also loads the extension with extension name MUSE_PIXTABLE_FF_EXT as
  table, and stores the content in the ffspec component. If this does not
  exist, this component remains NULL. If this extension exists but cannot be
  loaded, a warning is printed but no error is set, since this is not a
  critical failure.

  @error{return NULL\, propagate CPL error code,
         cpl_propertylist_load fails (this handles the case of an invalid or empty filename)}
  @error{return NULL\, propagate CPL error code,
         cpl_table_load fails to load table contents}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_load_window(const char *aFilename,
                          cpl_size aStart, cpl_size aNRows)
{
  /* create the output pixel table structure, and NULL out the components */
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));

  /* load the header and return with an error if something went wrong */
  cpl_errorstate prestate = cpl_errorstate_get();
  pt->header = cpl_propertylist_load(aFilename, 0);
  cpl_ensure(cpl_errorstate_is_equal(prestate) && pt->header,
             cpl_error_get_code(), NULL);
  if (muse_pixtable_get_type(pt) == MUSE_PIXTABLE_TYPE_UNKNOWN) {
    cpl_msg_error(__func__, "unknown pixel table type found in \"%s\"", aFilename);
    muse_pixtable_delete(pt);
    return NULL;
  }

  /* determine the type of data (FITS binary table extension or     *
   * FITS multi-extension images) by looking at the first extension */
  cpl_propertylist *hext = cpl_propertylist_load(aFilename, 1);
  cpl_boolean asimage = !strcmp(cpl_propertylist_get_string(hext, "XTENSION"),
                                "IMAGE");
  cpl_propertylist_delete(hext);

  /* load the table itself and return an error if something went wrong */
  if (asimage) {
    cpl_msg_info(__func__, "Loading pixel table \"%s\" (image format)",
                 aFilename);
    pt->table = muse_pixtable_load_window_image(aFilename, aStart, aNRows);
  } else {
    cpl_msg_info(__func__, "Loading pixel table \"%s\" (bintable format)",
                 aFilename);
    pt->table = cpl_table_load_window(aFilename, 1, 0, NULL, aStart, aNRows);
  }
  if (!cpl_errorstate_is_equal(prestate) || !pt->table) {
    cpl_msg_error(__func__, "Failed to load table part of pixel table \"%s\"",
                  aFilename);
    muse_pixtable_delete(pt);
    return NULL;
  }
  cpl_error_code rc = muse_cpltable_check(pt->table, muse_pixtable_def);
  if (rc != CPL_ERROR_NONE) {
    cpl_error_set_message(__func__, rc, "pixel table \"%s\" does not contain "
                          "all expected columns", aFilename);
  }

  /* load the flat-field spectrum, if it exists; same for both cases */
  muse_pixtable_load_ffspec(aFilename, pt);
  return pt;
} /* muse_pixtable_load_window() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Load the table itself and the FITS headers of a MUSE pixel table
           from a file.
  @param   aFilename   name of the file to load
  @return  a new muse_pixtable * or NULL on error
  @remark  The new table has to be deallocated using muse_pixtable_delete().

  This function tries to load the header from the FITS file and uses that to
  query the full length of the table before it calls @ref
  muse_pixtable_load_window() to actually load the complete pixel table.

  See @ref muse_pixtable_load_window() for the behavior regaring the flat-field
  spectrum.

  @error{return NULL\, propagate CPL error code,
         loading the binary table header fails}
  @error{return NULL\, propagate CPL error code,
         muse_pixtable_load_window() fails}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_load(const char *aFilename)
{
  /* find out original table length from the header of the FITS binary table */
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_propertylist *theader = cpl_propertylist_load(aFilename, 1);
  cpl_ensure(cpl_errorstate_is_equal(prestate) && theader,
             cpl_error_get_code(), NULL);
  cpl_size nrow = cpl_propertylist_get_long_long(theader, "NAXIS2");
  cpl_propertylist_delete(theader);

  /* now that we know the full size, load all rows */
  muse_pixtable *pt = muse_pixtable_load_window(aFilename, 0, nrow);
  return pt;
} /* muse_pixtable_load() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Load a pixel table from file and cut down the wavelength range.
  @param   aFilename    name of the file to load
  @param   aLambdaMin   low wavelength limit [Angstrom]
  @param   aLambdaMax   high wavelength limit [Angstrom]
  @return  a new muse_pixtable * or NULL on error

  Just a wrapper for muse_pixtable_load() followed by
  muse_pixtable_restrict_wavelength() and a few checks.

  @error{return NULL\, propagate CPL error code, muse_pixtable_load() fails}
  @error{return NULL\, propagate CPL error code,
         muse_pixtable_restrict_wavelength() fails}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         given reange is too short\, no pixels are left}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_load_restricted_wavelength(const char *aFilename,
                                         double aLambdaMin, double aLambdaMax)
{
  muse_pixtable *pt = muse_pixtable_load(aFilename);
  if (!pt) {
    return NULL;
  }
  cpl_error_code rc = muse_pixtable_restrict_wavelength(pt, aLambdaMin,
                                                        aLambdaMax);
  if (rc != CPL_ERROR_NONE) {
    muse_pixtable_delete(pt);
    return NULL;
  }
  if (muse_pixtable_get_nrow(pt) < 1) {
    cpl_msg_error(__func__, "Pixel table contains no entries after cutting to "
                  "%.3f..%.3f Angstrom", aLambdaMin, aLambdaMax);
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    muse_pixtable_delete(pt);
    return NULL;
  }
  return pt;
} /* muse_pixtable_load_restricted_wavelength() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Load and merge the pixel tables of the 24 MUSE sub-fields.
  @param   aExposureList   the one-row table listing all pixel table filenames
  @param   aLambdaMin      low wavelength limit [Angstrom]
  @param   aLambdaMax      high wavelength limit [Angstrom]
  @return  a muse_pixtable * containing the merged pixel table
  @remark  Uses the FITS header keyword ESO.DRS.MUSE.FLAT.FLUX.{SKY,LAMP} of all
           input pixel table headers to determine the relative flat-field
           level of each sub-field. This keyword is set by the routine that
           applies the flat-field exposure to other exposures.
  @remark This function adds a FITS header (@ref MUSE_HDR_PT_MERGED) with the
          number of merged IFUs to the pixel table, for information.

  This function simply loops through all filenames and loads the
  corresponding pixel table. For each pixel table it loops through all pixels
  and copies the data values into the output table. It uses the flat-field level
  relative to the first exposure to scale the data values across sub-fields to a
  common level.

  If the pixel tables have an attached flat-field spectrum, this is averaged
  over all exposures involved and applied to the merged pixel table. If only a
  subset of exposures comes with this flat-field spectrum, the function fails.
  The output flat-field spectrum is cut to about the same wavelength
  range as the pixel table itself, but keeping an approximate MUSE resolution
  element extra on each side, in case further resampling is necessary.
  If the flat-field spectrum was applied to the pixel table (i.e. it was taken
  out of the data), the keyword @ref MUSE_HDR_PT_FFCORR is set in the header of
  the output pixel table, as value it contains the number of pixel tables
  merged.

  @qa The basic operation of this routine is simple and will be tested
      using small and simple input tables. To be able to visually see the
      resulting table, one of the muse_resampling_<format> functions has to
      be used first, to create an output datacube.

  @error{leave the corresponding pixels out of the output table\, output warning\, but return the otherwise combined table,
         one or more of the input pixel tables of one exposure are missing}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         one or more of the input pixel tables correspond to data taken in a different instrument mode}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         the number of input pixel tables is different from the pixel tables with flat-field spectrum attached}
  @error{propagate error\, return partial pixel table,
         loading one of the input pixel tables fails}
  @error{set CPL_ERROR_FILE_NOT_FOUND\, return NULL,
         none of the pixel tables given in aExposureList could be loaded}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_load_merge_channels(cpl_table *aExposureList,
                                  double aLambdaMin, double aLambdaMax)
{
  cpl_ensure(aExposureList, CPL_ERROR_NULL_INPUT, NULL);

  muse_pixtable *pt = NULL; /* the big output pixel table */
  if (cpl_table_has_column(aExposureList, "00")) {
    const char *filename = cpl_table_get_string(aExposureList, "00", 0);
    if (filename) {
      pt = muse_pixtable_load_restricted_wavelength(filename, aLambdaMin,
                                                    aLambdaMax);
    }
    if (pt) {
      return pt;
    }
  } /* if table with "00" column */

  /* create array to hold interpolation points for flat-field spectrum */
  cpl_array *lambdas = cpl_array_new((kMuseLambdaMaxX - kMuseLambdaMinX)
                                     / kMuseSpectralSamplingA + 1,
                                     CPL_TYPE_DOUBLE),
            *ffspec = NULL; /* for output average flat-field spectrum data */
  int j, nwl = cpl_array_get_size(lambdas);
  for (j = 0; j < nwl; j++) {
    cpl_array_set_double(lambdas, j, kMuseLambdaMinX
                                     + j * kMuseSpectralSamplingA);
  }

  cpl_boolean isfirst = CPL_TRUE;
  double fluxlref = 0, /* propagated from lamp flat */
         fluxsref = 0; /* propagated from sky flat */
  int i, nifu = 0, nflats = 0;
  for (i = 1; i <= kMuseNumIFUs; i++) {
    char *colname = cpl_sprintf("%02d", i);
    const char *filename = cpl_table_get_string(aExposureList, colname, 0);
    cpl_free(colname);
    if (!filename) {
      cpl_msg_warning(__func__, "Channel for IFU %02d is missing", i);
      continue;
    }
    muse_pixtable *onept = muse_pixtable_load_restricted_wavelength(filename,
                                                                    aLambdaMin,
                                                                    aLambdaMax);
    if (!onept) {
      cpl_msg_error(__func__, "failed to load pixel table from \"%s\"",
                    filename);
      cpl_array_delete(lambdas);
      return pt;
    }
    nifu++;

    /* if this is the first pixel table, use its data to create the big one */
    if (isfirst) {
      pt = onept;
      cpl_msg_debug(__func__, "loaded pixel table with %"CPL_SIZE_FORMAT" rows",
                    muse_pixtable_get_nrow(pt));
      isfirst = CPL_FALSE;
      cpl_errorstate prestate = cpl_errorstate_get();
      fluxsref = cpl_propertylist_get_double(pt->header, MUSE_HDR_FLAT_FLUX_SKY);
      fluxlref = cpl_propertylist_get_double(pt->header, MUSE_HDR_FLAT_FLUX_LAMP);
      if (fluxsref == 0. && fluxlref == 0. && !cpl_errorstate_is_equal(prestate)) {
        /* If the flat headers are both missing, this can only mean that   *
         * the exposure was previously merged into the pixel table we just *
         * loaded. Then we can recover from the error and stop right here. */
        cpl_msg_debug(__func__, "\"%s\" was previously merged (got \"%s\" when"
                      " asking for flat-field fluxes)", filename,
                      cpl_error_get_message());
        cpl_errorstate_set(prestate);
        break;
      }
      if (fluxsref == 0. && fluxlref > 0. && !cpl_errorstate_is_equal(prestate)) {
        /* only the sky-flat level was missing, output a warning */
        cpl_msg_warning(__func__, "only found reference lamp-flat flux (%e) in "
                        "\"%s\", flux levels may vary between IFUs!", fluxlref,
                        filename);
        cpl_errorstate_set(prestate);
      } else {
        cpl_msg_debug(__func__, "reference flat fluxes sky: %e lamp: %e",
                      fluxsref, fluxlref);
      }
      cpl_propertylist_erase(pt->header, MUSE_HDR_FLAT_FLUX_SKY);
      cpl_propertylist_erase(pt->header, MUSE_HDR_FLAT_FLUX_LAMP);

      /* for the first table, directly resample the flat-field spectrum */
      if (pt->ffspec) { /* check, to keep backward compatibility */
        ffspec = muse_cplarray_interpolate_table_linear(lambdas, pt->ffspec,
                                                        "lambda", "data");
        /* delete the flat-field spectrum component, since    *
         * this will be the flat-field corrected output table */
        cpl_table_delete(pt->ffspec);
        pt->ffspec = NULL;
        nflats++;
      }
      continue;
    } /* if isfirst */

    /* copy the offset headers to the output pixel table */
    muse_pixtable_origin_copy_offsets(pt, onept, 0);

    /* to fix the flux offset (using propagated flat-field fluxes) */
    cpl_errorstate state = cpl_errorstate_get();
    double fluxs = cpl_propertylist_get_double(onept->header,
                                               MUSE_HDR_FLAT_FLUX_SKY),
           fluxl = cpl_propertylist_get_double(onept->header,
                                               MUSE_HDR_FLAT_FLUX_LAMP),
           scale = 1.;
    if (fluxsref > 0. && fluxs > 0.) { /* prefer sky flat flux scaling */
      scale = fluxs / fluxsref;
    } else if (fluxlref > 0. && fluxl > 0.) {
      scale = fluxl / fluxlref;
      if (!cpl_errorstate_is_equal(state)) {
        cpl_msg_warning(__func__, "only found relative lamp-flat flux (%e) in "
                        "\"%s\", flux levels may vary between IFUs!", fluxl,
                        filename);
        cpl_errorstate_set(state);
      } /* if bad error state */
    } /* else: use lamp-flat values */
    muse_pixtable_flux_multiply(onept, 1. / scale); /* inverse of scale here */

    /* resample ff-spectrum onto the regular sampling, given *
     * by the lambda vector created at the beginning         */
    if (onept->ffspec) { /* check, to keep backward compatibility */
      cpl_array *ffout = muse_cplarray_interpolate_table_linear(lambdas,
                                                                onept->ffspec,
                                                                "lambda", "data");
      if (ffspec) { /* if the first one had a flat-field spectrum */
        cpl_array_add(ffspec, ffout); /* add up to average the resulting spectrum */
      }
      cpl_array_delete(ffout);
      nflats++;
    } /* if ff-spectrum */

    /* append this pixel table to the big one */
    cpl_table_insert(pt->table, onept->table, muse_pixtable_get_nrow(pt));
    cpl_msg_debug(__func__, "big pixel table now has %"CPL_SIZE_FORMAT" entries,"
                  " scale was %e (flat fluxes sky: %e lamp: %e)",
                  muse_pixtable_get_nrow(pt), scale, fluxs, fluxl);

    /* the insertion duplicates the data, so we can delete it now */
    muse_pixtable_delete(onept);
  } /* for i (all IFUs) */

  /* If some but not all pixel tables came with the flat-field spectrum, *
   * we may get unforeseen results when doing flux calibration etc., so  *
   * better fail completely!                                             */
  if (nflats > 0 && nifu != nflats) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT, "Only %d of %d "
                          "pixel tables of this exposure came with a flat-field"
                          " spectrum, cannot continue!", nflats, nifu);
    cpl_array_delete(lambdas);
    cpl_array_delete(ffspec);
    muse_pixtable_delete(pt);
    return NULL;
  } /* if nflats */

  if (ffspec) {
    /* we need the average flat-field spectrum, so     *
     * divide by the number of IFUs that were combined */
    cpl_array_divide_scalar(ffspec, nflats);
    cpl_msg_debug(__func__, "Average of flat-field spectrum: %.4f",
                  cpl_array_get_mean(ffspec));
    /* convert flat-field spectrum to table, to be able to smooth it *
     * and to save it later                                          */
    muse_table *fftable = muse_table_new();
    fftable->header = cpl_propertylist_duplicate(pt->header);
    cpl_size nrow = cpl_array_get_size(lambdas);
    fftable->table = cpl_table_new(nrow);
    cpl_table_new_column(fftable->table, MUSE_PIXTABLE_FFLAMBDA, CPL_TYPE_DOUBLE);
    cpl_table_new_column(fftable->table, MUSE_PIXTABLE_FFDATA, CPL_TYPE_DOUBLE);
    muse_cpltable_copy_array(fftable->table, MUSE_PIXTABLE_FFLAMBDA, lambdas);
    muse_cpltable_copy_array(fftable->table, MUSE_PIXTABLE_FFDATA, ffspec);
    cpl_array_delete(ffspec);
#if 0
    muse_table_save(fftable, "fftable_unsmoothed.fits");
#endif
    /* smooth the flat-field spectrum, for all instrument modes */
    cpl_table_duplicate_column(fftable->table, "data_unsm", fftable->table, "data");
    muse_utils_spectrum_smooth(fftable, MUSE_SPECTRUM_SMOOTH_PPOLY);
#if 0
    muse_table_save(fftable, "fftable_smoothed.fits");
#endif
    /* set it up as array again... */
    double *buffer = cpl_table_get_data_double(fftable->table,
                                               MUSE_PIXTABLE_FFDATA);
    ffspec = cpl_array_wrap_double(buffer, nrow);

    /* now correct the output pixel table by the averaged flat-field spectrum */
    muse_pixtable_spectrum_apply(pt, lambdas, ffspec,
                                 MUSE_PIXTABLE_OPERATION_MULTIPLY);

    /* mark the header as having gotten the correction */
    cpl_propertylist_update_int(pt->header, MUSE_HDR_PT_FFCORR, nflats);
    cpl_propertylist_set_comment(pt->header, MUSE_HDR_PT_FFCORR,
                                 MUSE_HDR_PT_FFCORR_COMMENT);

    /* keep the (rather small) flat-field spectrum around, *
     * at least for debugging, but remove the array        */
    pt->ffspec = cpl_table_duplicate(fftable->table);
    cpl_array_unwrap(ffspec);
    muse_table_delete(fftable);
    /* everything before was done on a fixed grid to encompass       *
     * all possible MUSE data, now we have to get rid of the invalid *
     * entries at the extreme ends of the flat-field spectrum        */
    cpl_table_erase_invalid(pt->ffspec); // XXX
   } /* if ffspec */
  cpl_array_delete(lambdas);

  /* now that we merged all pixel tables, compute the full extent */
  muse_pixtable_compute_limits(pt);
  if (!pt) {
    cpl_error_set_message(__func__, CPL_ERROR_FILE_NOT_FOUND,
                          "None of the pixel tables could be loaded");
    return NULL; /* exit here to not crash below! */
  }
  /* data does not belong to any one IFU any more, so *
   * remove the EXTNAME header that contains CHAN%02d */
  cpl_propertylist_erase_regexp(pt->header, "^EXTNAME|"MUSE_HDR_PT_ILLUM_REGEXP,
                                0);
  /* also remove chip-specific info that doesn't belong to the merged data */
  cpl_propertylist_erase_regexp(pt->header, "ESO DET (CHIP|OUT) ", 0);
  cpl_propertylist_erase_regexp(pt->header, "ESO DET2 ", 0);
  /* add the status header */
  cpl_propertylist_update_int(pt->header, MUSE_HDR_PT_MERGED, nifu);
  cpl_propertylist_set_comment(pt->header, MUSE_HDR_PT_MERGED,
                               MUSE_HDR_PT_MERGED_COMMENT);
  return pt;
} /* muse_pixtable_load_merge_channels() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine the type of pixel table.
  @param   aPixtable   the pixel table to query
  @return  a muse_pixtable_type on success or MUSE_PIXTABLE_TYPE_UNKNOWN on
           failure

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_PIXTABLE_TYPE_UNKNOWN,
         aPixtable is NULL}
  @error{return MUSE_PIXTABLE_TYPE_UNKNOWN\, propagate CPL error code,
         cpl_propertylist_get_string fails to return the header parameter}
 */
/*---------------------------------------------------------------------------*/
int
muse_pixtable_get_type(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, MUSE_PIXTABLE_TYPE_UNKNOWN);
  const char *type = cpl_propertylist_get_string(aPixtable->header,
                                                 MUSE_HDR_PT_TYPE);
#if 0
  cpl_msg_debug(__func__, "pixel table type \"%s\"", type);
#endif
  if (!type) {
    return MUSE_PIXTABLE_TYPE_UNKNOWN;
  }
  if (!strncmp(type, MUSE_PIXTABLE_STRING_FULL,
               strlen(MUSE_PIXTABLE_STRING_FULL) + 1)) {
    return MUSE_PIXTABLE_TYPE_FULL;
  } else if (!strncmp(type, MUSE_PIXTABLE_STRING_SIMPLE,
                      strlen(MUSE_PIXTABLE_STRING_SIMPLE) + 1)) {
    return MUSE_PIXTABLE_TYPE_SIMPLE;
  }

  return MUSE_PIXTABLE_TYPE_UNKNOWN;
} /* muse_pixtable_get_type() */

/*---------------------------------------------------------------------------*/
/**
  @brief   get the number of rows within the pixel table
  @param   aPixtable   the pixel table
  @return The number of rows or 0 on error

  This function is a simple wrapper for cpl_table_get_nrow() to abstract out
  the internal pixel table structure.
 */
/*---------------------------------------------------------------------------*/
cpl_size
muse_pixtable_get_nrow(const muse_pixtable *aPixtable)
{
  /* make sure that we are getting valid input */
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, 0);
  cpl_ensure(aPixtable->table, CPL_ERROR_NULL_INPUT, 0);

  /* let CPL do the work */
  return cpl_table_get_nrow(aPixtable->table);
} /* muse_pixtable_get_nrow() */

/*---------------------------------------------------------------------------*/
/**
  @brief  (Re-)Compute the limits of the coordinate columns of a pixel table.
  @param  aPixtable   the pixel table
  @retval CPL_ERROR_NONE on success

  This function searches the pixel table for the extreme values of each
  coordinate.
  The limits it finds are returned in the header of the pixel table in the
  keywords containing "MUSEPT" (see muse_pixtable.h).

  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable or one of its components are NULL}
  @error{return CPL_ERROR_DATA_NOT_FOUND,
         the table component of aPixtable does not have a pixel table structure}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_compute_limits(muse_pixtable *aPixtable)
{
  /* make sure that we are getting valid input */
  cpl_ensure_code(aPixtable && aPixtable->table && aPixtable->header,
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_cpltable_check(aPixtable->table, muse_pixtable_def)
                  == CPL_ERROR_NONE, CPL_ERROR_DATA_NOT_FOUND);

  if (muse_pixtable_get_nrow(aPixtable) == 0) {
    return CPL_ERROR_NONE;
  }

  float *cdata_xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *cdata_ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *cdata_lambda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  uint32_t *origin = (uint32_t *)cpl_table_get_data_int(aPixtable->table,
                                                        MUSE_PIXTABLE_ORIGIN);
  /* set to extreme values for a start */
  float xlo = FLT_MAX, xhi = -FLT_MAX,
        ylo = FLT_MAX, yhi = -FLT_MAX,
        llo = FLT_MAX, lhi = -FLT_MAX;
  int ifulo = INT_MAX, ifuhi = 0,
      slicelo = INT_MAX, slicehi = 0;
  cpl_size i, nrow = muse_pixtable_get_nrow(aPixtable);
  for (i = 0; i < nrow; i++) {
    if (cdata_xpos[i] > xhi) xhi = cdata_xpos[i];
    if (cdata_xpos[i] < xlo) xlo = cdata_xpos[i];
    if (cdata_ypos[i] > yhi) yhi = cdata_ypos[i];
    if (cdata_ypos[i] < ylo) ylo = cdata_ypos[i];
    if (cdata_lambda[i] > lhi) lhi = cdata_lambda[i];
    if (cdata_lambda[i] < llo) llo = cdata_lambda[i];
    int ifu = muse_pixtable_origin_get_ifu_fast(origin[i]),
        slice = muse_pixtable_origin_get_slice_fast(origin[i]);
    if (ifu > ifuhi) ifuhi = ifu;
    if (ifu < ifulo) ifulo = ifu;
    if (slice > slicehi) slicehi = slice;
    if (slice < slicelo) slicelo = slice;
  } /* for i */
  char *dodebug = getenv("MUSE_DEBUG_PIXTABLE_LIMITS");
  if (dodebug && atoi(dodebug)) {
    cpl_msg_debug(__func__, "x: %f...%f, y: %f...%f, lambda: %f...%f, "
                  "ifu: %d...%d, slice: %d...%d",
                  xlo, xhi, ylo, yhi, llo, lhi, ifulo, ifuhi, slicelo, slicehi);
  }
  cpl_propertylist_erase_regexp(aPixtable->header, MUSE_HDR_PT_LIMITS_REGEXP, 0);
  double ptxoff = 0., /* zero by default ...   */
         ptyoff = 0.; /* for pixel coordinates */
  if (muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH) {
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_XLO, xlo + ptxoff);
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_XHI, xhi + ptxoff);
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_YLO, ylo + ptyoff);
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_YHI, yhi + ptyoff);
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_LLO, llo);
  cpl_propertylist_append_float(aPixtable->header, MUSE_HDR_PT_LHI, lhi);
  cpl_propertylist_append_int(aPixtable->header, MUSE_HDR_PT_ILO, ifulo);
  cpl_propertylist_append_int(aPixtable->header, MUSE_HDR_PT_IHI, ifuhi);
  cpl_propertylist_append_int(aPixtable->header, MUSE_HDR_PT_SLO, slicelo);
  cpl_propertylist_append_int(aPixtable->header, MUSE_HDR_PT_SHI, slicehi);

  return CPL_ERROR_NONE;
} /* muse_pixtable_compute_limits() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Scale the flux of a pixel table with correct treatment of variance.
  @param   aPixtable   pixel table to be changed
  @param   aScale      the flux scale factor
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Scaling here is meant to be multiplicative with the input aScale factor.

  This function assumes that the scale is corrective, so that it leaves keywords
  related to flux units untouched.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aPixtable or its table component are NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_flux_multiply(muse_pixtable *aPixtable, double aScale)
{
  cpl_ensure_code(aPixtable && aPixtable->table, CPL_ERROR_NULL_INPUT);
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_table_multiply_scalar(aPixtable->table, MUSE_PIXTABLE_DATA, aScale);
  cpl_table_multiply_scalar(aPixtable->table, MUSE_PIXTABLE_STAT, aScale*aScale);
  cpl_error_code rc = CPL_ERROR_NONE;
  if (!cpl_errorstate_is_equal(prestate)) {
    rc = cpl_error_get_code();
  }
  return rc;
} /* muse_pixtable_flux_multiply() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Apply a spectrum given by two arrays with an operation to a pixel
           table.
  @param   aPixtable   pixel table to be changed
  @param   aLambdas    wavelength array of the spectrum
  @param   aFlux       flux array of the spectrum
  @param   aOperation   the operation to be performed
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Supported operations so far are MUSE_PIXTABLE_OPERATION_SUBTRACT,
  MUSE_PIXTABLE_OPERATION_MULTIPLY, and MUSE_PIXTABLE_OPERATION_DIVIDE, any
  other aOperation leads to an error.

  The arrays aLambdas and aFlux are expected to be of type CPL_TYPE_DOUBLE,
  and have to have the same lengths. These arrays are used to linearly
  interpolate the spectrum to the exact wavelengths of the pixels in the pixel
  table before applying the values to all pixels with the given aOperation.

  This function assumes that the operation is corrective, so that it leaves
  keywords related to flux units untouched.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aPixtable or its table component are NULL}
  @error{set and return CPL_ERROR_NULL_INPUT,
         aLambdas and/or aFlux are NULL}
  @error{set and return CPL_ERROR_ILLEGAL_INPUT,
         the lengths of aLambdas and/or aFlux are not equal or not positive}
  @error{set and return CPL_ERROR_INCOMPATIBLE_INPUT,
         the types of aLambdas and/or aFlux are not CPL_TYPE_DOUBLE}
  @error{set and return CPL_ERROR_UNSUPPORTED_MODE,
         aOperation is not MUSE_PIXTABLE_OPERATION_SUBTRACT or MUSE_PIXTABLE_OPERATION_MULTIPLY}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_spectrum_apply(muse_pixtable *aPixtable,
                             const cpl_array *aLambdas, const cpl_array *aFlux,
                             muse_pixtable_operation aOperation)
{
  cpl_ensure_code(aPixtable && aPixtable->table, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aLambdas && aFlux, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(cpl_array_get_size(aLambdas) > 0 &&
                  cpl_array_get_size(aLambdas) == cpl_array_get_size(aFlux),
                  CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure_code(cpl_array_get_type(aLambdas) == CPL_TYPE_DOUBLE &&
                  cpl_array_get_type(aFlux) == CPL_TYPE_DOUBLE,
                  CPL_ERROR_INCOMPATIBLE_INPUT);

  /* To apply a given spectrum, decompose the full pixel table into per-slice *
   * pixel tables, which can be sorted by wavelength.  For these, we can then *
   * linearly interpolate the input spectrum onto the same wavelengths as in  *
   * the sub-pixel table and then subtract that in the form on an array.      *
   * This also has the advantage of being parallelizable.                     */
  muse_pixtable **slicepts = muse_pixtable_extracted_get_slices(aPixtable);
  cpl_size islice, nslices = muse_pixtable_extracted_get_size(slicepts);

  switch (aOperation) {
  case MUSE_PIXTABLE_OPERATION_SUBTRACT:
    cpl_msg_debug(__func__, "Subtracting spectrum from pixel table with %"
                  CPL_SIZE_FORMAT" slices...", nslices);
    break;
  case MUSE_PIXTABLE_OPERATION_MULTIPLY:
    cpl_msg_debug(__func__, "Multiplying pixel table of %"CPL_SIZE_FORMAT
                  " slices with spectrum...", nslices);
    break;
  case MUSE_PIXTABLE_OPERATION_DIVIDE:
    cpl_msg_debug(__func__, "Dividing pixel table of %"CPL_SIZE_FORMAT
                  " slices with spectrum...", nslices);
    break;
  default:
    muse_pixtable_extracted_delete(slicepts);
    return cpl_error_set(__func__, CPL_ERROR_UNSUPPORTED_MODE);
  } /* switch */

  /* now loop through all extracted slices in parallel */
  #pragma omp parallel for default(none)                                       \
          shared(aFlux, aLambdas, aOperation, nslices, slicepts)
  for (islice = 0; islice < nslices; islice++) {
    muse_pixtable *thispt = slicepts[islice]; /* one slice pixel table */
    cpl_propertylist *order = cpl_propertylist_new();
    cpl_propertylist_append_bool(order, MUSE_PIXTABLE_LAMBDA, CPL_FALSE);
    cpl_table_sort(thispt->table, order);
    cpl_propertylist_delete(order);

    /* get sorted wavelengths as double array (so    *
     * that the interpolation function can be used!) */
    cpl_table_cast_column(thispt->table, MUSE_PIXTABLE_LAMBDA, "lbda_d",
                          CPL_TYPE_DOUBLE);
    cpl_array *lambdapt = muse_cpltable_extract_column(thispt->table, "lbda_d");
    cpl_array *fint = muse_cplarray_interpolate_linear(lambdapt, aLambdas,
                                                       aFlux);

    /* get relevant pixel table columns to apply the flat-field spectrum to */
    cpl_array *datapt = muse_cpltable_extract_column(thispt->table,
                                                     MUSE_PIXTABLE_DATA),
              *statpt = muse_cpltable_extract_column(thispt->table,
                                                     MUSE_PIXTABLE_STAT);
    /* apply the flat-field spectrum */
    if (aOperation == MUSE_PIXTABLE_OPERATION_SUBTRACT) {
      cpl_array_subtract(datapt, fint);
    } else if (aOperation == MUSE_PIXTABLE_OPERATION_DIVIDE) {
      cpl_array_divide(datapt, fint);
      /* divide the variance squared (i.e. twice) */
      cpl_array_divide(statpt, fint);
      cpl_array_divide(statpt, fint);
    } else { /* MUSE_PIXTABLE_OPERATION_MULTIPLY */
      cpl_array_multiply(datapt, fint);
      /* multiply to the variance squared (i.e. twice) */
      cpl_array_multiply(statpt, fint);
      cpl_array_multiply(statpt, fint);
    } /* if aOperation */
    cpl_array_delete(fint);

    cpl_array_unwrap(datapt);
    cpl_array_unwrap(statpt);
    cpl_array_unwrap(lambdapt);
    cpl_table_erase_column(thispt->table, "lbda_d");
  } /* for islice (all slice pixel tables) */
  muse_pixtable_extracted_delete(slicepts);

  return CPL_ERROR_NONE;
} /* muse_pixtable_spectrum_apply() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Fix the exposure ranges in the header of a pixel table.
  @param   aPixtable the pixel table
  @retval  CPL_ERROR_NONE on success, another cpl_error_code on failure.

  This function assumes that all selected rows are going to be erased, and
  resets the header keywords MUSE_HDR_PT_EXP_FST and MUSE_HDR_PT_EXP_LST
  accordingly.
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_pixtable_fix_exp_headers(muse_pixtable *aPixtable)
{
  cpl_ensure_code(aPixtable && aPixtable->header && aPixtable->table,
                  CPL_ERROR_NULL_INPUT);
  if (cpl_table_count_selected(aPixtable->table) < 1) {
    return CPL_ERROR_NONE; /* nothing to do */
  }
  cpl_array *sel = cpl_table_where_selected(aPixtable->table);
  cpl_size narray = cpl_array_get_size(sel),
           nselprev = 0, /* selected ones in previous exposures */
           ifst = 0, ilst = 0;
  const cpl_size *asel = cpl_array_get_data_cplsize_const(sel);
  unsigned int nexp = 0;
  do {
    /* get exposure range for (next) exposure */
    char *kwfst = cpl_sprintf(MUSE_HDR_PT_EXP_FST, ++nexp),
         *kwlst = cpl_sprintf(MUSE_HDR_PT_EXP_LST, nexp);
    if (!cpl_propertylist_has(aPixtable->header, kwfst) ||
        !cpl_propertylist_has(aPixtable->header, kwlst)) {
      cpl_free(kwfst);
      cpl_free(kwlst);
      break; /* nothing (more) to do */
    }
    ifst = cpl_propertylist_get_long_long(aPixtable->header, kwfst);
    ilst = cpl_propertylist_get_long_long(aPixtable->header, kwlst);
    /* count selected */
    cpl_size i, nsel = 0;
    for (i = 0; i < narray; i++) {
      if (asel[i] >= ifst && asel[i] <= ilst) {
        nsel++;
      } /* if in range of this exposure */
    } /* for i (all array entries) */
    cpl_size ifst2 = ifst - nselprev,
             ilst2 = ilst - nsel - nselprev;
    cpl_msg_debug(__func__, "exp %d old %"CPL_SIZE_FORMAT"..%"CPL_SIZE_FORMAT
                  ", %"CPL_SIZE_FORMAT" selected (previous: %"CPL_SIZE_FORMAT
                  "), new %"CPL_SIZE_FORMAT"..%"CPL_SIZE_FORMAT, nexp,
                  ifst, ilst, nsel, nselprev, ifst2, ilst2);
    /* finally update the header entries */
    muse_cplpropertylist_update_long_long(aPixtable->header, kwfst, ifst2);
    muse_cplpropertylist_update_long_long(aPixtable->header, kwlst, ilst2);
    cpl_free(kwfst);
    cpl_free(kwlst);
    nselprev += nsel; /* add selected ones for previous exposures */
  } while (ilst >= ifst);
  cpl_array_delete(sel);
  return CPL_ERROR_NONE;
} /* muse_pixtable_fix_exp_headers() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Restrict a pixel table to a certain wavelength range.
  @param   aPixtable the pixel table
  @param   aLow      low wavelength limit [Angstrom]
  @param   aHigh     high wavelength limit [Angstrom]
  @retval CPL_ERROR_NONE Everything went OK.

  Cut the data in the pixel table to the exact range [aLow, aHigh].

  If it exists cut the ffspec component of aPixtable to [aLow-2.5, aHigh+2.5]
  to allow for more interpolation at the edges.

  The exact wavelength range of entries left in aPixtable->table (and
  aPixtable->ffspec) of course depends on the incoming entries.

  @cpl_ensure{aPixtable && aPixtable->table, CPL_ERROR_NULL_INPUT, NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_restrict_wavelength(muse_pixtable *aPixtable, double aLow,
                                  double aHigh)
{
  cpl_ensure_code(aPixtable && aPixtable->table && aPixtable->header,
                  CPL_ERROR_NULL_INPUT);
  if (cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LLO) > aLow &&
      cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LHI) < aHigh) {
    /* no need to do anything */
    return CPL_ERROR_NONE;
  }
#pragma omp critical(cpl_table_select)
  {
    cpl_table_unselect_all(aPixtable->table);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA,
                                CPL_LESS_THAN, aLow);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA,
                                CPL_GREATER_THAN, aHigh);
    muse_pixtable_fix_exp_headers(aPixtable);
    cpl_table_erase_selected(aPixtable->table);
  }

  /* do the same for the flat-field spectrum, if it exists, but leave a *
   * spectral resolution element more on each side, for interpolation   */
#pragma omp critical(cpl_table_select)
  if (aPixtable->ffspec) {
    cpl_table_unselect_all(aPixtable->ffspec);
    cpl_table_or_selected_double(aPixtable->ffspec, MUSE_PIXTABLE_FFLAMBDA,
                                 CPL_LESS_THAN,
                                 aLow - 2. * kMuseSpectralSamplingA);
    cpl_table_or_selected_double(aPixtable->ffspec, MUSE_PIXTABLE_FFLAMBDA,
                                 CPL_GREATER_THAN,
                                 aHigh + 2. * kMuseSpectralSamplingA);
    cpl_table_erase_selected(aPixtable->ffspec);
  }

  return muse_pixtable_compute_limits(aPixtable);
} /* muse_pixtable_restrict_wavelength() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Restrict a pixel table to a certain x coordinate range.
  @param   aPixtable   the pixel table
  @param   aLo         low xpos limit
  @param   aHi         high xpos limit
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable or one of its components is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_restrict_xpos(muse_pixtable *aPixtable, double aLo, double aHi)
{
  cpl_ensure_code(aPixtable && aPixtable->table && aPixtable->header,
                  CPL_ERROR_NULL_INPUT);
  if (cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XLO) > aLo &&
      cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XHI) < aHi) {
    /* no need to do anything */
    return CPL_ERROR_NONE;
  }
  double ptxoff = 0.;
  if (muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH) {
    /* need to use the real coordinate offset for celestial spherical */
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
  }
#pragma omp critical(cpl_table_select)
  {
    cpl_table_unselect_all(aPixtable->table);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_XPOS,
                                CPL_LESS_THAN, aLo - ptxoff);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_XPOS,
                                CPL_GREATER_THAN, aHi - ptxoff);
    muse_pixtable_fix_exp_headers(aPixtable);
    cpl_table_erase_selected(aPixtable->table);
  }
  return muse_pixtable_compute_limits(aPixtable);
} /* muse_pixtable_restrict_xpos() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Restrict a pixel table to a certain y coordinate range.
  @param   aPixtable   the pixel table
  @param   aLo         low ypos limit
  @param   aHi         high ypos limit
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable or one of its components is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_restrict_ypos(muse_pixtable *aPixtable, double aLo, double aHi)
{
  cpl_ensure_code(aPixtable && aPixtable->table && aPixtable->header,
                  CPL_ERROR_NULL_INPUT);
  if (cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YLO) > aLo &&
      cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YHI) < aHi) {
    /* no need to do anything */
    return CPL_ERROR_NONE;
  }
  double ptyoff = 0.;
  if (muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH) {
    /* need to use the real coordinate offset for celestial spherical */
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
#pragma omp critical(cpl_table_select)
  {
    cpl_table_unselect_all(aPixtable->table);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_YPOS,
                                CPL_LESS_THAN, aLo - ptyoff);
    cpl_table_or_selected_float(aPixtable->table, MUSE_PIXTABLE_YPOS,
                                CPL_GREATER_THAN, aHi - ptyoff);
    muse_pixtable_fix_exp_headers(aPixtable);
    cpl_table_erase_selected(aPixtable->table);
  }
  return muse_pixtable_compute_limits(aPixtable);
} /* muse_pixtable_restrict_ypos() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Erase pixel table rows related to one slice of one IFU.
  @param   aPixtable   the pixel table
  @param   aIFU        the IFU number
  @param   aSlice      the slice number
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  This function selects all rows in the given pixel table which originated in
  aIFU and aSlice. Then all selected slices are erased.

  @error{return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
  @error{return CPL_ERROR_DATA_NOT_FOUND, no rows found in aPixtable}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_erase_ifu_slice(muse_pixtable *aPixtable, unsigned char aIFU,
                              unsigned short aSlice)
{
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  cpl_ensure_code(nrow > 0, CPL_ERROR_DATA_NOT_FOUND);

  cpl_table_unselect_all(aPixtable->table);
  uint32_t *origin = (uint32_t *)cpl_table_get_data_int(aPixtable->table,
                                                        MUSE_PIXTABLE_ORIGIN);
  cpl_size irow;
  for (irow = 0; irow < nrow; irow++) {
    unsigned char ifu = muse_pixtable_origin_get_ifu(origin[irow]);
    unsigned short slice = muse_pixtable_origin_get_slice(origin[irow]);
    if (ifu == aIFU && slice == aSlice) {
      cpl_table_select_row(aPixtable->table, irow);
    } /* if same IFU and slice */
  } /* for irow (all pixtable rows) */
  cpl_size nsel = cpl_table_count_selected(aPixtable->table);
  cpl_error_code rc = cpl_table_erase_selected(aPixtable->table);
  cpl_msg_debug(__func__, "Erased %"CPL_SIZE_FORMAT" rows from pixel table",
                nsel);

  muse_pixtable_fix_exp_headers(aPixtable);
  muse_pixtable_compute_limits(aPixtable);
  return rc;
} /* muse_pixtable_erase_ifu_slice() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Select all pixels where the spatial positions are enabled in the
           given mask.
  @param   aPixtable        The pixel table
  @param   aMask            The mask to select pixels
  @param   aWCS             header containing a MUSE astrometric solution
  @param   aOffsets         table with exposure position offsets
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  This function modifies the existing table selection in aPixtable such that it
  unselects pixels that are spatially located in areas where aMask is unselected
  as well. The spatial correspondence is established either with a celestial
  gnomonic projection (then, aWCS and aOffsets should be passed) or a simple
  linear transformation without crossterms. Which of these applies is detemrined
  from the world coordinates present in the header of aMask.

  Pixels which are outside of the mask are not selected at all.

  @cpl_ensure_code{aPixtable && aPixtable->table, CPL_ERROR_NULL_INPUT}
  @cpl_ensure_code{aMask, CPL_ERROR_NULL_INPUT}
*/
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_and_selected_mask(muse_pixtable *aPixtable,
                                const muse_mask *aMask,
                                const cpl_propertylist *aWCS,
                                const cpl_table *aOffsets)
{
  cpl_ensure_code(aPixtable && aPixtable->table, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aMask && aMask->mask && aMask->header, CPL_ERROR_NULL_INPUT);

  /* first create WCS object from input mask header for easy access */
  cpl_errorstate state = cpl_errorstate_get();
  muse_wcs *maskwcs = muse_wcs_new(aMask->header);
  if (!maskwcs || !cpl_errorstate_is_equal(state)) {
    cpl_msg_error(__func__, "Selecting pixel table rows using mask failed due "
                  "to faulty WCS in mask header: %s", cpl_error_get_message());
    cpl_free(maskwcs);
    return cpl_error_get_code();
  }

  /* Check of which type the WCS is and which unit it has. This determines   *
   * how we then need to process the pixel table data to be able to mask it. */
  const char *ctype1 = muse_pfits_get_ctype(aMask->header, 1),
             *ctype2 = muse_pfits_get_ctype(aMask->header, 2),
             *cunit1 = muse_pfits_get_cunit(aMask->header, 1),
             *cunit2 = muse_pfits_get_cunit(aMask->header, 2);
  maskwcs->iscelsph = (ctype1 && strlen(ctype1) >= 5 && ctype1[4] == '-')
                    && (ctype2 && strlen(ctype2) >= 5 && ctype2[4] == '-');
  /* check that the possible celestial WCS is gnomonic */
  if (maskwcs->iscelsph && (!strstr(ctype1, "-TAN") || !strstr(ctype2, "-TAN"))) {
    cpl_msg_warning(__func__, "Cannot use mask WCS for pixel selection, "
                    "unsupported celestial WCS (%s / %s)", ctype1, ctype2);
    return CPL_ERROR_UNSUPPORTED_MODE;
  }
  /* check that the unit is "deg" if the WCS is celestial */
  if (maskwcs->iscelsph && ((cunit1 && strncmp(cunit1, "deg", 4)) ||
                            (cunit2 && strncmp(cunit2, "deg", 4)))) {
    cpl_msg_warning(__func__, "Cannot use mask WCS for pixel selection, "
                    "celestial gnomonic WCS with unsupported units (%s / %s)",
                    cunit1, cunit2);
    return CPL_ERROR_UNSUPPORTED_MODE;
  }

  /* If the mask comes with celestial WCS, we need to duplicate and     *
   * project/position the pixel table before we can check, which pixels *
   * need to be selected. This is not necessary for linear WCS.         */
  muse_pixtable *pt = aPixtable; /* non-celestial case */
  double ra = 0., dec = 0.;
  if (maskwcs->iscelsph) {
    pt = muse_pixtable_duplicate(aPixtable);
    if (aWCS) {
      muse_wcs_project_tan(pt, aWCS);
    } else {
      const char *mode = muse_pfits_get_insmode(pt->header);
      cpl_msg_warning(__func__, "Using default MUSE %cFM astrometry, pixel "
                      "selection using mask will be inaccurate!", mode[0]);
      cpl_propertylist *wcs = muse_wcs_create_default(pt->header);
      muse_wcs_project_tan(pt, wcs);
      cpl_propertylist_delete(wcs);
    }
    muse_postproc_offsets_scale(pt, aOffsets, "single pixel table for masking");
    if (!aOffsets) {
      cpl_msg_warning(__func__, "Using mask with celestial WCS for pixel "
                      "selection, but no %s was given. Results will likely "
                      "be inaccurate!", MUSE_TAG_OFFSET_LIST);
    }
    ra = muse_pfits_get_ra(pt->header);
    dec = muse_pfits_get_dec(pt->header);
    muse_wcs_position_celestial(pt, ra, dec);

    /* convert to radians, for muse_wcs_pixel_from_celestial_fast() */
    maskwcs->crval1 /= CPL_MATH_DEG_RAD;
    maskwcs->crval2 /= CPL_MATH_DEG_RAD;
  } /* if celestial */

  /* Use x- and y-positions of the possibly transformed pixel table. */
  float *xpos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_YPOS);
  cpl_size nx = cpl_mask_get_size_x(aMask->mask),
           ny = cpl_mask_get_size_y(aMask->mask);
  const cpl_binary *maskdata = cpl_mask_get_data_const(aMask->mask);

  cpl_size irow, nrows = cpl_table_get_nrow(aPixtable->table),
           nsel = cpl_table_count_selected(aPixtable->table),
           nenabled = cpl_mask_count(aMask->mask),
           narea = 0;
  cpl_msg_debug(__func__, "Mask contains %"CPL_SIZE_FORMAT" (%.2f %%) enabled "
                "pixels of %"CPL_SIZE_FORMAT" total [%s WCS, %s/%s, units "
                "%s/%s]", nenabled, 100.*nenabled/nx/ny, nx*ny,
                maskwcs->iscelsph ? "celestial (gnomonic)" : "linear",
                ctype1, ctype2, cunit1, cunit2);
#if PIXTABLE_MASK_PARALLEL
  #pragma omp parallel for default(none)                                       \
          shared(aPixtable, dec, maskdata, maskwcs, narea, nrows, nsel, nx, ny,\
                 ra, xpos, ypos)
#endif
  for (irow = 0; irow < nrows; irow++) {
    /* Compute pixel location in mask on possibly transformed pixel table. */
    double x, y;
    if (maskwcs->iscelsph) {
      /* Add RA/DEC zeropoints before the conversion and convert to radians; *
       * the crval were already converted to radians above.                  */
      muse_wcs_pixel_from_celestial_fast(maskwcs,
                                         (xpos[irow] + ra) * CPL_MATH_RAD_DEG,
                                         (ypos[irow] + dec) * CPL_MATH_RAD_DEG,
                                         &x, &y);
    } else { /* linear case */
      muse_wcs_pixel_from_projplane_fast(maskwcs, xpos[irow], ypos[irow], &x, &y);
    }
    int ix = lround(x),
        iy = lround(y);
    if ((ix < 1) || (ix > nx) || (iy < 1) || (iy > ny)) {
      continue;
    }
#if PIXTABLE_MASK_PARALLEL
    #pragma omp atomic
#endif
    narea++; /* at least this pixel is inside the mask area */
    /* Apply selection always on the original pixel table. */
    if (maskdata[(ix-1) + (iy-1) * nx] != CPL_BINARY_1) {
#if PIXTABLE_MASK_PARALLEL
      #pragma omp critical
#endif
      {
      cpl_table_unselect_row(aPixtable->table, irow);
      nsel--;
      }
    } /* if */
  } /* for all pixel table rows */
  if (maskwcs->iscelsph) {
    /* need to delete temporary pixel table again */
    muse_pixtable_delete(pt);
  }
  cpl_free(maskwcs);
  cpl_msg_debug(__func__, "Mask selected %"CPL_SIZE_FORMAT" (%.2f %%/%.2f %%) "
                "pixels of %"CPL_SIZE_FORMAT" total/%"CPL_SIZE_FORMAT" in mask "
                "area", nsel, 100.*nsel/nrows, 100.*nsel/narea, nrows, narea);
  return CPL_ERROR_NONE;
} /* muse_pixtable_and_selected_mask() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Dump a MUSE pixel table to the screen, resolving the origin column.
  @param   aPixtable        the pixel table
  @param   aStart           first row to print (starting at 0)
  @param   aCount           number of rows to print
  @param   aDisplayHeader   if 1, a header is displayed above the table;
                            if greater than 1, it does not include units
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  This function basically does the same as cpl_table_dump(), with two
  differences: it can resolve the origin column to the real on-CCD origin and
  it can list exposures numbers that are only available in the pixel table
  headers.

  It also tries to convert the CCD coordinates (after trimming) to raw CCD
  coordinates (before trimming). But this only works under the assumption that
  all over- and pre-scans are kMusePreOverscanSize pixels wide and each quadrant
  is (kMuseOutputXRight/2)x(kMuseOutputYTop/2) pixels in size.

  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable or one of its components is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         aStart is negative or larger than the number of rows in the table\, or aCount is negative}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         one of the 6 mandatory columns is missing}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_dump(muse_pixtable *aPixtable, cpl_size aStart, cpl_size aCount,
                   unsigned char aDisplayHeader)
{
  cpl_ensure_code(aPixtable && aPixtable->table && aPixtable->header,
                  CPL_ERROR_NULL_INPUT);
  cpl_size nrows = muse_pixtable_get_nrow(aPixtable);
  cpl_ensure_code(aStart >= 0 && aStart < nrows && aCount >= 0,
                  CPL_ERROR_ILLEGAL_INPUT);
  cpl_size last = aStart + aCount;
  if (last > nrows - 1) {
    last = nrows;
  }
  int haswcs  = muse_pixtable_wcs_check(aPixtable);
  double ptxoff = 0., ptyoff = 0.;
  if (haswcs == MUSE_PIXTABLE_WCS_CELSPH) {
    /* need to use the real coordinate offset for celestial spherical */
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
  /* for non-pixel coordinates, we will need different formats below */
  haswcs = (haswcs == MUSE_PIXTABLE_WCS_NATSPH ||
            haswcs == MUSE_PIXTABLE_WCS_CELSPH);
  float *cdata_xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *cdata_ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *cdata_lambda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *cdata_data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *cdata_stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
  cpl_errorstate es = cpl_errorstate_get();
  float *cdata_weight = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_WEIGHT);
  cpl_errorstate_set(es); /* ignore errors due to missing weight column */
  int *cdata_dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);
  uint32_t *cdata_origin = (uint32_t *)cpl_table_get_data_int(aPixtable->table,
                                                              MUSE_PIXTABLE_ORIGIN);
  cpl_ensure_code(cdata_xpos && cdata_ypos && cdata_lambda &&
                  cdata_data && cdata_dq && cdata_stat,
                  CPL_ERROR_BAD_FILE_FORMAT);

  /* print header */
  if (aDisplayHeader) {
    printf("# xpos          ypos           lambda     data        dq         stat"
           "         weight     exposure IFU xCCD yCCD xRaw yRaw slice\n");
  }
  if (aDisplayHeader == 1) {
    printf("#%13s  %13s %9s   %11s       flag %11s  ----------   No     No   pix "
           " pix  pix  pix No\n# flux    in [%s]\n# flux**2 in [%s]\n",
           cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS),
           cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_YPOS),
           cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
           "(flux)", "(flux**2)",
           cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_DATA),
           cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_STAT));
  }

  cpl_size i;
  for (i = aStart; i < last; i++) {
    int x = muse_pixtable_origin_get_x(cdata_origin[i], aPixtable, i),
        y = muse_pixtable_origin_get_y_fast(cdata_origin[i]),
        xraw = x, yraw = y;
    muse_quadrants_coords_to_raw(NULL, &xraw, &yraw);
    if (haswcs) {
      printf("%14.7e %14.7e %9.3f  ", cdata_xpos[i] + ptxoff, cdata_ypos[i] + ptyoff,
             cdata_lambda[i]);
    } else {
      printf("%14.8f %14.8f %9.3f  ", cdata_xpos[i], cdata_ypos[i], cdata_lambda[i]);
    }
    printf("%12.5e 0x%08x %11.5e  %10.4e   %2d     %2d %4d %4d %4d %4d  %2d\n",
           cdata_data[i], cdata_dq[i], cdata_stat[i],
           cdata_weight ? cdata_weight[i] : 0.,
           muse_pixtable_get_expnum(aPixtable, i),
           cdata_origin ? muse_pixtable_origin_get_ifu_fast(cdata_origin[i])
                        : 0,
           x, y, xraw, yraw,
           cdata_origin ? muse_pixtable_origin_get_slice_fast(cdata_origin[i])
                        : 0);
  } /* for i (pixel table rows) */

  return CPL_ERROR_NONE;
} /* muse_pixtable_dump() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Check the state of the world coordinate system of a pixel table.
  @param   aPixtable   the pixel table
  @return  the WCS state or MUSE_PIXTABLE_WCS_UNKNOWN on error

  This function check the column units of the MUSE_PIXTABLE_XPOS and
  MUSE_PIXTABLE_YPOS pixel table columns, to check the state of the WCS of the
  pixel table.
  If no column unit was set at all or xpos and ypos have different units,
  someone fiddled with the pixel table. In that case, we may get unwanted side
  effects, so set an error state.

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_PIXTABLE_WCS_UNKNOWN,
         aPixtable is NULL}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return MUSE_PIXTABLE_WCS_UNKNOWN,
         coordinate column without unit or not present}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return MUSE_PIXTABLE_WCS_UNKNOWN,
         coordinate column units for x and y are different}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return MUSE_PIXTABLE_WCS_UNKNOWN,
         coordinate column unit is neither "pix" nor "deg" or "rad"}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable_wcs
muse_pixtable_wcs_check(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, MUSE_PIXTABLE_WCS_UNKNOWN);
  const char *unitx = cpl_table_get_column_unit(aPixtable->table,
                                                MUSE_PIXTABLE_XPOS),
             *unity = cpl_table_get_column_unit(aPixtable->table,
                                                MUSE_PIXTABLE_YPOS);
  cpl_ensure(unitx, CPL_ERROR_DATA_NOT_FOUND, MUSE_PIXTABLE_WCS_UNKNOWN);
  cpl_ensure(unity, CPL_ERROR_DATA_NOT_FOUND, MUSE_PIXTABLE_WCS_UNKNOWN);
  /* should be equal in the first three characters */
  cpl_ensure(!strncmp(unitx, unity, 4), CPL_ERROR_INCOMPATIBLE_INPUT,
             MUSE_PIXTABLE_WCS_UNKNOWN);
  if (!strncmp(unitx, "deg", 4)) {
    return MUSE_PIXTABLE_WCS_CELSPH;
  }
  if (!strncmp(unitx, "pix", 4)) {
    return MUSE_PIXTABLE_WCS_PIXEL;
  }
  if (!strncmp(unitx, "rad", 4)) {
    return MUSE_PIXTABLE_WCS_NATSPH;
  }
  cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
  return MUSE_PIXTABLE_WCS_UNKNOWN;
} /* muse_pixtable_wcs_check() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine whether the pixel table is flux calibrated.
  @param   aPixtable   the pixel table
  @return  CPL_TRUE if the pixel table is flux calibrated.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_FALSE, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_boolean
muse_pixtable_is_fluxcal(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_boolean flag = cpl_propertylist_get_bool(aPixtable->header,
                                               MUSE_HDR_PT_FLUXCAL);
  cpl_errorstate_set(prestate);
  return flag;
}

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine whether the pixel table is sky subtracted.
  @param   aPixtable   the pixel table
  @return  CPL_TRUE if the pixel table is sky subtracted.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_FALSE, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_boolean
muse_pixtable_is_skysub(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_boolean flag = cpl_propertylist_get_bool(aPixtable->header,
                                               MUSE_HDR_PT_SKYSUB);
  cpl_errorstate_set(prestate);
  return flag;
}

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine whether the pixel table is radial-velocity corrected.
  @param   aPixtable   the pixel table
  @return  CPL_TRUE if the pixel table is radial-velocity corrected.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_FALSE, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_boolean
muse_pixtable_is_rvcorr(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  return cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_RVCORR);
} /* muse_pixtable_is_rvcorr() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Reset a given bad pixel status (DQ flag) for all pixels in the table.
  @param   aPixtable   the pixel table
  @param   aDQ         the (Euro3D) flag to reset
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  @error{return CPL_ERROR_NULL_INPUT, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_reset_dq(muse_pixtable *aPixtable, unsigned int aDQ)
{
  cpl_ensure_code(aPixtable, CPL_ERROR_NULL_INPUT);

  unsigned int *dq = (unsigned int *)cpl_table_get_data_int(aPixtable->table,
                                                            MUSE_PIXTABLE_DQ),
               inverse = ~aDQ; /* inverse bitmask to AND each row's DQ with */
  cpl_size i, nrow = muse_pixtable_get_nrow(aPixtable);
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(dq, inverse, nrow)
  for (i = 0; i < nrow; i++) {
    dq[i] &= inverse;
  } /* for i (all table rows) */
  return CPL_ERROR_NONE;
} /* muse_pixtable_reset_dq() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Project a pixel table with data from one IFU back onto its image.
  @param   aPixtable   the pixel table
  @return  A muse_imagelist on success or NULL on error.

  For each processed slice, an approximation of its horizontal center is
  written to the header of the respective image, in the form
  ESO.DRS.MUSE.SLICEi.CENTER (as float).
  The DQ extension for the gaps between slices and for locations not recorded
  in the input pixel table is set to EURO3D_MISSDATA.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aPixtable or its header component are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aPixtable contains data from more than one exposure}
 */
/*---------------------------------------------------------------------------*/
muse_imagelist *
muse_pixtable_to_imagelist(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable && aPixtable->header, CPL_ERROR_NULL_INPUT, NULL);
  unsigned int expnum = muse_pixtable_get_expnum(aPixtable, 0),
               explast = muse_pixtable_get_expnum(aPixtable,
                                                  muse_pixtable_get_nrow(aPixtable) - 1);
  cpl_ensure(expnum == explast, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* data seems to be valid, we can create the output list */
  muse_imagelist *list = muse_imagelist_new();

  /* split the pixel table up into per-slice pixel tables to handle    *
   * them separately (to get the x-range of each slice for the header) */
  muse_pixtable **pts = muse_pixtable_extracted_get_slices(aPixtable);
  /* variables for current image and current IFU */
  muse_image *image = NULL;
  unsigned short ifu = 0, /* we haven't found any IFU yet */
                 ilist = 0; /* image index in the list */
  int ipt, npt = muse_pixtable_extracted_get_size(pts);
  for (ipt = 0; ipt < npt; ipt++) {
    float *cdata = cpl_table_get_data_float(pts[ipt]->table, MUSE_PIXTABLE_DATA),
          *cstat = cpl_table_get_data_float(pts[ipt]->table, MUSE_PIXTABLE_STAT);
    int *cdq = cpl_table_get_data_int(pts[ipt]->table, MUSE_PIXTABLE_DQ);
    uint32_t *corigin = (uint32_t *)cpl_table_get_data_int(pts[ipt]->table,
                                                           MUSE_PIXTABLE_ORIGIN);
    /* if we got to the next (or first) IFU, create new output *
     * image of the size of a typical MUSE CCD copy the header *
     * from the pixel table but remove specific entries        */
    if (ifu != muse_pixtable_origin_get_ifu_fast(corigin[0])) {
      image = muse_image_new();
      image->header = cpl_propertylist_duplicate(pts[ipt]->header);
      cpl_propertylist_erase_regexp(image->header, "^ESO DRS MUSE PIXTABLE", 0);
      image->data = cpl_image_new(kMuseOutputXRight, kMuseOutputYTop, CPL_TYPE_FLOAT);
      image->dq = cpl_image_new(kMuseOutputXRight, kMuseOutputYTop, CPL_TYPE_INT);
      /* fill DQ image with EURO3D_MISSDATA, upper value will be cast correctly */
      cpl_image_fill_noise_uniform(image->dq, EURO3D_MISSDATA, EURO3D_MISSDATA + 0.1);
      image->stat = cpl_image_new(kMuseOutputXRight, kMuseOutputYTop, CPL_TYPE_FLOAT);
      cpl_msg_debug(__func__, "new image (index %hu in list)", ilist);
      muse_imagelist_set(list, image, ilist++);
    } /* if ifu */
    if (!image) { /* it cannot really go wrong, but to be sure... */
      cpl_msg_error(__func__, "ipt = %d: no image!", ipt);
      continue;
    }
    float *idata = cpl_image_get_data_float(image->data),
          *istat = cpl_image_get_data_float(image->stat);
    int *idq = cpl_image_get_data_int(image->dq);

    ifu = muse_pixtable_origin_get_ifu_fast(corigin[0]);
    unsigned short slice = muse_pixtable_origin_get_slice_fast(corigin[0]);
    unsigned int xoff = muse_pixtable_origin_get_offset(pts[ipt], expnum, ifu,
                                                        slice),
                 x1 = INT_MAX, x2 = 0,
                 irow, nrow = muse_pixtable_get_nrow(pts[ipt]);
    for (irow = 0; irow < nrow; irow++) {
      /* get coordinate indices from the origin column */
      unsigned int x = muse_pixtable_origin_get_x_fast(corigin[irow], xoff) - 1,
                   y = muse_pixtable_origin_get_y_fast(corigin[irow]) - 1;
      idata[x + y*kMuseOutputXRight] = cdata[irow];
      idq[x + y*kMuseOutputXRight] = cdq[irow];
      istat[x + y*kMuseOutputXRight] = cstat[irow];
      if (x < x1) {
        x1 = x;
      }
      if (x > x2) {
        x2 = x;
      }
    } /* for irow */
    /* record the approximate horizontal center in the output header */
    char *keyword = cpl_sprintf("ESO DRS MUSE SLICE%hu CENTER", slice);
    cpl_propertylist_update_float(image->header, keyword, (x2 + x1) / 2. + 1.);
#if 0
    cpl_msg_debug(__func__, "IFU %hu %s = %.1f", ifu, keyword,
                  (x2 + x1) / 2. + 1.);
#endif
    cpl_free(keyword);
  } /* for ipt */
  muse_pixtable_extracted_delete(pts);

  return list;
} /* muse_pixtable_to_imagelist() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get pixel table values back from a per-IFU imagelist.
  @param   aPixtable   the pixel table
  @param   aList       the image list
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  This function basically does the inverse of muse_pixtable_to_imagelist(): it
  assumes that the input image list was created by that function, and processed
  in some way, that did not affect the pixel positions. Then it does the same
  split into per-IFU / per-slice subtables and reads the respective pixel
  values and their variance back from the images, using the location saved in
  the MUSE_PIXTABLE_ORIGIN column.

  @note This directly modifies the input pixel table, i.e. replaces the values
        in the two data columns.
  @note The DQ values in the images in the list are ignored, the contents of the
        MUSE_PIXTABLE_DQ column of the pixel table remains unchanged.

  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable or its header component are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         aPixtable contains data from more than one exposure}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         aPixtable and aList contain a different number of IFUs}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_pixtable_from_imagelist(muse_pixtable *aPixtable, muse_imagelist *aList)
{
  cpl_ensure_code(aPixtable && aPixtable->header && aList, CPL_ERROR_NULL_INPUT);
  unsigned int expnum = muse_pixtable_get_expnum(aPixtable, 0),
               explast = muse_pixtable_get_expnum(aPixtable,
                                                  muse_pixtable_get_nrow(aPixtable) - 1);
  cpl_ensure_code(expnum == explast, CPL_ERROR_ILLEGAL_INPUT);

  /* split the pixel table up into per-slice pixel tables   *
   * as in muse_pixtable_to_imagelist() and ensure that the *
   * number of IFUs matches the number of input images      */
  muse_pixtable **pts = muse_pixtable_extracted_get_slices(aPixtable);
  if (muse_pixtable_extracted_get_size(pts) / kMuseSlicesPerCCD
      != muse_imagelist_get_size(aList)) {
    muse_pixtable_extracted_delete(pts);
    return cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
  }
  /* variables for current image and current IFU */
  muse_image *image = NULL;
  unsigned short ifu = 0, /* we haven't worked with any IFU yet */
                 ilist = 0; /* image index in the list */
  int ipt, npt = muse_pixtable_extracted_get_size(pts);
  for (ipt = 0; ipt < npt; ipt++) {
    float *cdata = cpl_table_get_data_float(pts[ipt]->table, MUSE_PIXTABLE_DATA),
          *cstat = cpl_table_get_data_float(pts[ipt]->table, MUSE_PIXTABLE_STAT);
    uint32_t *corigin = (uint32_t *)cpl_table_get_data_int(pts[ipt]->table,
                                                           MUSE_PIXTABLE_ORIGIN);
    /* if we got to the next (or first) IFU, get an image from the list */
    if (ifu != muse_pixtable_origin_get_ifu_fast(corigin[0])) {
      image = muse_imagelist_get(aList, ilist++);
    } /* if ifu */
    if (!image) { /* it cannot really go wrong, but to be sure... */
      cpl_msg_error(__func__, "ipt = %d: no image!", ipt);
      continue;
    }
    float *idata = cpl_image_get_data_float(image->data),
          *istat = cpl_image_get_data_float(image->stat);
    ifu = muse_pixtable_origin_get_ifu_fast(corigin[0]);
    unsigned short slice = muse_pixtable_origin_get_slice_fast(corigin[0]);
    unsigned int xoff = muse_pixtable_origin_get_offset(pts[ipt], expnum, ifu,
                                                        slice),
                 irow, nrow = muse_pixtable_get_nrow(pts[ipt]);
    for (irow = 0; irow < nrow; irow++) {
      /* get coordinate indices from the origin column */
      unsigned int x = muse_pixtable_origin_get_x_fast(corigin[irow], xoff) - 1,
                   y = muse_pixtable_origin_get_y_fast(corigin[irow]) - 1;
      cdata[irow] = idata[x + y*kMuseOutputXRight];
      cstat[irow] = istat[x + y*kMuseOutputXRight];
    } /* for irow */
  } /* for ipt */
  muse_pixtable_extracted_delete(pts);

  return CPL_ERROR_NONE;
} /* muse_pixtable_from_imagelist() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Create a new pixtable with values within the selected wavelengths
  @param   aPixtable   the pixel table
  @param   aLow        the lower limit of the wavelength
  @param   aHigh       the upper limit of the wavelength
  @return  Pointer to the new pixtable, or NULL on error

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable *
muse_pixtable_extract_wavelength(muse_pixtable *aPixtable, double aLow,
                                  double aHigh)
{
  cpl_ensure(aPixtable && aPixtable->table && aPixtable->header,
             CPL_ERROR_NULL_INPUT, NULL);
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));
  pt->header = cpl_propertylist_duplicate(aPixtable->header);

#pragma omp critical(cpl_table_select)
  {
    cpl_table_select_all(aPixtable->table);
    cpl_table_and_selected_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA,
                                 CPL_NOT_LESS_THAN, aLow);
    cpl_table_and_selected_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA,
                                 CPL_NOT_GREATER_THAN, aHigh);
    pt->table = cpl_table_extract_selected(aPixtable->table);
    cpl_table_select_all(aPixtable->table);
  }

  muse_pixtable_compute_limits(pt);
  return pt;
} /* muse_pixtable_extract_wavelength() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Extract one pixel table per IFU and slice.
  @param   aPixtable   the pixel table
  @return  NULL terminated array of slice pixel tables or NULL on error

  The pixel table is re-sorted for slice and IFU.  The slice pixel tables are
  not copied from the original table but share the data in memory. So they
  should not be deleted individually but with the function
  muse_pixtable_extracted_delete().

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
muse_pixtable **
muse_pixtable_extracted_get_slices(muse_pixtable *aPixtable)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, NULL);
  cpl_size n_rows = cpl_table_get_nrow(aPixtable->table);
  unsigned int ifu_slice_mask = (0x1f << MUSE_ORIGIN_SHIFT_IFU) | 0x3f;
  cpl_table_duplicate_column(aPixtable->table, "ifuslice",
                             aPixtable->table, MUSE_PIXTABLE_ORIGIN);
  unsigned int *slicedata = (unsigned int *)
    cpl_table_get_data_int(aPixtable->table, "ifuslice");
  cpl_size i_row;
  unsigned int last_ifu_slice = 0;
  int is_sorted = CPL_TRUE;
  for (i_row = 0; i_row < n_rows; i_row++) {
    slicedata[i_row] &= ifu_slice_mask;
    if (is_sorted && slicedata[i_row] < last_ifu_slice) {
      is_sorted = CPL_FALSE;
    } else {
      last_ifu_slice = slicedata[i_row];
    }
  }
  if (!is_sorted) {
    cpl_propertylist *order = cpl_propertylist_new();
    cpl_propertylist_append_bool(order, "ifuslice", CPL_FALSE);
    cpl_propertylist_append_bool(order, MUSE_PIXTABLE_LAMBDA, CPL_FALSE);
    cpl_msg_debug(__func__, "sorting pixel table: quick sort, %"CPL_SIZE_FORMAT
                  " entries", n_rows);
    cpl_table_sort(aPixtable->table, order);
    cpl_propertylist_delete(order);
    /* erase headers that depend on the order in the pixel table */
    cpl_propertylist_erase_regexp(aPixtable->header, MUSE_HDR_PT_EXP_REGEXP, 0);
    cpl_msg_debug(__func__, "pixel table sorted.");
  } /* if !sorted */

  i_row = 0;
  cpl_size n_col = cpl_table_get_ncol(aPixtable->table);
  cpl_array *colnames = cpl_table_get_column_names(aPixtable->table);
  muse_pixtable **slice_tables = cpl_calloc(1, sizeof(muse_pixtable *));
  cpl_size n_slices = 0;
  while (i_row < n_rows) {
    unsigned int ifu_slice = slicedata[i_row];
    cpl_size j_row;
    for (j_row = i_row+1; j_row < n_rows && slicedata[j_row] == ifu_slice;
         j_row++)
      ;
    cpl_size nrows_slice = j_row - i_row;
    muse_pixtable *slice_pixtable = cpl_calloc(1, sizeof(muse_pixtable));
    slice_pixtable->table = cpl_table_new(nrows_slice);
    cpl_size i_col;
    for (i_col = 0; i_col < n_col; i_col++) {
      const char *cname = cpl_array_get_string(colnames, i_col);
      if (strcmp(cname, "ifuslice") == 0)
        continue;
      cpl_type ctype = cpl_table_get_column_type(aPixtable->table, cname);
      if (ctype == CPL_TYPE_INT) {
        int *cdata = cpl_table_get_data_int(aPixtable->table, cname);
        cpl_table_wrap_int(slice_pixtable->table, cdata + i_row, cname);
      } else if (ctype == CPL_TYPE_FLOAT) {
        float *cdata = cpl_table_get_data_float(aPixtable->table, cname);
        cpl_table_wrap_float(slice_pixtable->table, cdata + i_row, cname);
      } else if (ctype == CPL_TYPE_DOUBLE) {
        double *cdata = cpl_table_get_data_double(aPixtable->table, cname);
        cpl_table_wrap_double(slice_pixtable->table, cdata + i_row, cname);
      } else if (ctype == CPL_TYPE_STRING) {
        char **cdata = cpl_table_get_data_string(aPixtable->table, cname);
        cpl_table_wrap_string(slice_pixtable->table, cdata + i_row, cname);
      }
      const char *unit = cpl_table_get_column_unit(aPixtable->table, cname);
      cpl_table_set_column_unit(slice_pixtable->table, cname, unit);
    } /* for i_col (all table columns) */

    slice_pixtable->header = cpl_propertylist_duplicate(aPixtable->header);
    muse_pixtable_compute_limits(slice_pixtable);
    slice_tables = cpl_realloc(slice_tables,
                               (n_slices + 2) * sizeof(muse_pixtable *));
    slice_tables[n_slices] = slice_pixtable;
    n_slices++;
    slice_tables[n_slices] = NULL;
    i_row = j_row;
  } /* while (all full pixel table rows) */
  cpl_array_delete(colnames);
  cpl_table_erase_column(aPixtable->table, "ifuslice");

  return slice_tables;
} /* muse_pixtable_extracted_get_slices() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Get the size of an array of extracted pixel tables.
  @param   aPixtables   The pixel table array.
  @return  The number of pixel tables in this array, a negative value on error.

  Since the table array is NULL terminated, it just searches for the NULL
  entry.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aPixtable is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_size
muse_pixtable_extracted_get_size(muse_pixtable **aPixtables)
{
  cpl_ensure(aPixtables, CPL_ERROR_NULL_INPUT, -1);
  cpl_size n = 0;
  while (aPixtables[n] != NULL) {
    n++;
  }
  return n;
} /* muse_pixtable_extracted_get_size() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Delete a pixel table array.
  @param   aPixtables   The pixel table array.

  This deletes only the array itself, not the columns, since they are
  shared with the "mother" pixel table.

  @see muse_pixtable_extract_slices()
 */
/*---------------------------------------------------------------------------*/
void
muse_pixtable_extracted_delete(muse_pixtable **aPixtables)
{
  if (!aPixtables) {
    return;
  }
  muse_pixtable **t;
  for (t = aPixtables; *t != NULL; t++) {
    cpl_array *colnames = cpl_table_get_column_names((*t)->table);
    cpl_size n_col = cpl_table_get_ncol((*t)->table);
    cpl_size i_col;
    for (i_col = 0; i_col < n_col; i_col++) {
      const char *cname = cpl_array_get_string(colnames, i_col);
      cpl_table_unwrap((*t)->table, cname);
    }
    cpl_array_delete(colnames);
    cpl_table_delete((*t)->table);
    cpl_propertylist_delete((*t)->header);
    cpl_free(*t);
  }
  cpl_free(aPixtables);
} /* muse_pixtable_extracted_delete() */

/**@}*/
