MUSE Pipeline Reference Manual  2.1.1
muse_geo.c
1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set sw=2 sts=2 et cin: */
3 /*
4  * This file is part of the MUSE Instrument Pipeline
5  * Copyright (C) 2005-2015 European Southern Observatory
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  */
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24 
25 /*----------------------------------------------------------------------------*
26  * Includes *
27  *----------------------------------------------------------------------------*/
28 #define _BSD_SOURCE /* get setenv() from stdlib.h */
29 #include <stdlib.h> /* setenv() */
30 
31 #include <cpl.h>
32 #include <float.h>
33 #include <math.h>
34 #include <string.h>
35 #include <cpl.h>
36 
37 #include "muse_geo.h"
38 #include "muse_instrument.h"
39 
40 #include "muse_astro.h"
41 #include "muse_cplwrappers.h"
42 #include "muse_data_format_z.h"
43 #include "muse_dfs.h"
44 #include "muse_pfits.h"
45 #include "muse_tracing.h"
46 #include "muse_utils.h"
47 #include "muse_wavecalib.h"
48 
49 /*----------------------------------------------------------------------------*/
57 /*----------------------------------------------------------------------------*/
60 /*----------------------------------------------------------------------------*/
89 /*----------------------------------------------------------------------------*/
91  { "filename", CPL_TYPE_STRING, "", "%s",
92  "(raw) filename from which this measurement originates", CPL_TRUE },
93  { "image", CPL_TYPE_INT, "", "%03d", "number of the image in the series", CPL_TRUE },
94  { "POSENC2", CPL_TYPE_INT, "", "%d",
95  "x position of the mask in encoder steps", CPL_TRUE },
96  { "POSPOS2", CPL_TYPE_DOUBLE, "mm", "%.3f", "x position of the mask", CPL_TRUE },
97  { "POSENC3", CPL_TYPE_INT, "", "%d",
98  "y position of the mask in encoder steps", CPL_TRUE },
99  { "POSPOS3", CPL_TYPE_DOUBLE, "mm", "%.3f", "y position of the mask", CPL_TRUE },
100  { "POSENC4", CPL_TYPE_INT, "", "%d",
101  "z position of the mask in encoder steps", CPL_TRUE },
102  { "POSPOS4", CPL_TYPE_DOUBLE, "mm", "%.3f", "z position of the mask", CPL_TRUE },
103  { "VPOS", CPL_TYPE_DOUBLE, "mm", "%.3f", "real vertical position of the mask", CPL_TRUE },
104  { "ScaleFOV", CPL_TYPE_DOUBLE, "arcsec/mm", "%.3f",
105  "focus scale in VLT focal plane (from the FITS header)", CPL_TRUE },
106  { "SubField", CPL_TYPE_INT, "", "%02d", "sub-field number", CPL_TRUE },
107  { "SliceCCD", CPL_TYPE_INT, "", "%02d",
108  "slice number as counted on the CCD", CPL_TRUE },
109  { "lambda", CPL_TYPE_DOUBLE, "Angstrom", "%.3f", "wavelength", CPL_TRUE },
110  { "SpotNo", CPL_TYPE_INT, "", "%04d",
111  "number of this spot within the slice (1 is left, 2 is the central one, 3 is right within the slice)", CPL_TRUE },
112  { "xc", CPL_TYPE_DOUBLE, "pix", "%.3f", "x center of this spot on the CCD", CPL_TRUE },
113  { "yc", CPL_TYPE_DOUBLE, "pix", "%.3f", "y center of this spot on the CCD", CPL_TRUE },
114  { "xfwhm", CPL_TYPE_DOUBLE, "pix", "%.2f", "FWHM in x-direction on the CCD", CPL_TRUE },
115  { "yfwhm", CPL_TYPE_DOUBLE, "pix", "%.2f", "FWHM in y-direction on the CCD", CPL_TRUE },
116  { "flux", CPL_TYPE_DOUBLE, "", "%.1f",
117  "flux of the spot as integrated on the CCD image", CPL_TRUE },
118  { "bg", CPL_TYPE_DOUBLE, "", "%f", "background level around the spot", CPL_TRUE },
119  { "dxcen", CPL_TYPE_DOUBLE, "pix", "%f",
120  "distance to center of slice at vertical position yc (positive: right of center)", CPL_TRUE },
121  { "twidth", CPL_TYPE_DOUBLE, "pix", "%f",
122  "trace width of the slice at the vertical CCD position of the spot", CPL_TRUE },
123  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
124 };
125 
126 /*----------------------------------------------------------------------------*/
168 /*----------------------------------------------------------------------------*/
170  { MUSE_GEOTABLE_FIELD, CPL_TYPE_INT, "", "%02d",
171  "sub-field (IFU / channel) number", CPL_TRUE },
172  { MUSE_GEOTABLE_CCD, CPL_TYPE_INT, "", "%02d",
173  "the slice number on the CCD, counted from left to right", CPL_TRUE },
174  { MUSE_GEOTABLE_SKY, CPL_TYPE_INT, "", "%02d",
175  "the slice number on the sky", CPL_TRUE },
176  { MUSE_GEOTABLE_X, CPL_TYPE_DOUBLE, "pix", "%9.4f",
177  "x position within field of view", CPL_TRUE },
178  { MUSE_GEOTABLE_Y, CPL_TYPE_DOUBLE, "pix", "%9.4f",
179  "y position within field of view", CPL_TRUE },
180  { MUSE_GEOTABLE_ANGLE, CPL_TYPE_DOUBLE, "deg", "%6.3f",
181  "rotation angle of slice", CPL_TRUE },
182  { MUSE_GEOTABLE_WIDTH, CPL_TYPE_DOUBLE, "pix", "%.2f",
183  "width of slice within field of view", CPL_TRUE },
184  { MUSE_GEOTABLE_X"err", CPL_TYPE_DOUBLE, "pix", "%8.4f",
185  "error estimated of x position within field of view", CPL_TRUE },
186  { MUSE_GEOTABLE_Y"err", CPL_TYPE_DOUBLE, "pix", "%8.4f",
187  "error estimate of y position within field of view", CPL_TRUE },
188  { MUSE_GEOTABLE_ANGLE"err", CPL_TYPE_DOUBLE, "deg", "%.3f",
189  "error estimate of rotation angle", CPL_TRUE },
190  { MUSE_GEOTABLE_WIDTH"err", CPL_TYPE_DOUBLE, "pix", "%.2f",
191  "error estimate of slice width", CPL_TRUE },
192  { "stack", CPL_TYPE_INT, "", "%02d",
193  "slicer stack that this slice belongs to (optical numbering)", CPL_TRUE },
194  { "spot", CPL_TYPE_INT, "", "%1d", "spot number in this slice", CPL_TRUE },
195  { "xrel", CPL_TYPE_DOUBLE, "mm", "%7.4f",
196  "x offset of this spot relative to the slice center", CPL_TRUE },
197  { "xrelerr", CPL_TYPE_DOUBLE, "mm", "%6.4f",
198  "error of the relative x offset of this spot", CPL_TRUE },
199  { "xc", CPL_TYPE_DOUBLE, "pix", "%.3f", "x center of this spot on the CCD", CPL_TRUE },
200  { "yc", CPL_TYPE_DOUBLE, "pix", "%.3f", "y center of this spot on the CCD", CPL_TRUE },
201  { "dxl", CPL_TYPE_DOUBLE, "pix", "%.3f", "distance to left edge of slice on the CCD", CPL_TRUE },
202  { "dxr", CPL_TYPE_DOUBLE, "pix", "%.3f", "distance to right edge of slice on the CCD", CPL_TRUE },
203  { "dx", CPL_TYPE_DOUBLE, "pix", "%.3f", "pinhole distance in x on the CCD", CPL_TRUE },
204  { "dxerr", CPL_TYPE_DOUBLE, "pix", "%.3f",
205  "error estimate of the pinhole distance in x on the CCD", CPL_TRUE },
206  { "vpos", CPL_TYPE_DOUBLE, "mm", "%.4f",
207  "(averaged) vertical position of the mask", CPL_TRUE },
208  { "vposerr", CPL_TYPE_DOUBLE, "mm", "%.4f",
209  "error estimated of the (averaged) vertical position of the mask", CPL_TRUE },
210  { "flux", CPL_TYPE_DOUBLE, "", "%.1f",
211  "flux of the spot as integrated on the CCD image", CPL_TRUE },
212  { "lambda", CPL_TYPE_DOUBLE, "Angstrom", "%.3f", "wavelength", CPL_TRUE },
213  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
214 };
215 
216 /*----------------------------------------------------------------------------*/
233 /*----------------------------------------------------------------------------*/
234 cpl_table *
235 muse_geo_table_extract_ifu(const cpl_table *aTable, const unsigned char aIFU)
236 {
237  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, NULL);
238  cpl_ensure(aIFU >= 1 && aIFU <= kMuseNumIFUs, CPL_ERROR_ILLEGAL_INPUT, NULL);
239 
240  /* duplicate the input table so that it's not changed */
241  cpl_table *intable = cpl_table_duplicate(aTable);
242 
243  /* Sort the table by subfield and then slice number on CCD (both *
244  * ascending); the sort order was verified using optical model and INM. */
245  cpl_propertylist *sorting = cpl_propertylist_new();
246  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
247  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_CCD, CPL_FALSE);
248  cpl_table_sort(intable, sorting);
249  cpl_propertylist_delete(sorting);
250 
251  cpl_table_select_all(intable);
252  cpl_table_and_selected_int(intable, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, aIFU);
253  cpl_table *subtable = cpl_table_extract_selected(intable);
254  cpl_table_delete(intable);
255 #if 0
256  printf("table (extracted for IFU %2d)\n", aIFU);
257  cpl_table_dump(subtable, 0, 100000, stdout);
258  fflush(stdout);
259 #endif
260  int nrow = cpl_table_get_nrow(subtable);
261  if (nrow != kMuseSlicesPerCCD) {
262  cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT,
263  "geometry table contains %d instead of %d slices for "
264  "IFU %d", nrow, kMuseSlicesPerCCD, aIFU);
265  cpl_table_delete(subtable);
266  subtable = NULL;
267  } /* if nrow */
268  return subtable;
269 } /* muse_geo_table_extract_ifu() */
270 
271 /*----------------------------------------------------------------------------*/
295 /*----------------------------------------------------------------------------*/
296 double
297 muse_geo_table_ifu_area(const cpl_table *aTable, const unsigned char aIFU,
298  double aScale)
299 {
300  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, 0.);
301 
302  /* create table containing only the entries for the given IFU */
303  cpl_table *table = muse_geo_table_extract_ifu(aTable, aIFU);
304  /* we need the standard number of slices otherwise the result is very wrong */
305  cpl_size nrow = cpl_table_get_nrow(table);
306  cpl_ensure(nrow == kMuseSlicesPerCCD, CPL_ERROR_ILLEGAL_INPUT, 0.);
307 
308  /* sort the table by the slice number on the sky, to *
309  * be able to directly access the relevant entries */
310  cpl_propertylist *sorting = cpl_propertylist_new();
311  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
312  cpl_table_sort(table, sorting);
313  cpl_propertylist_delete(sorting);
314 
315  /* now go through the slicer stacks one by one, sum up the widths and *
316  * compute the vertical size of the stack, which is 11x the slice height */
317  double area = 0., areas[4];
318  int istack,
319  nperstack = kMuseSlicesPerCCD / 4; /* number of slices per slicer stack */
320  for (istack = 0; istack < 4; istack++) {
321  cpl_table *stack = cpl_table_extract(table, 12 * istack, nperstack);
322  /* get the sizes in pix units from the table, then convert them *
323  * to cm, using the scale factors used elsewhere in this module */
324  double height = fabs(cpl_table_get(stack, MUSE_GEOTABLE_Y, 0, NULL)
325  - cpl_table_get(stack, MUSE_GEOTABLE_Y, nperstack - 1, NULL))
326  / (nperstack - 1.) /* height [pix] */
327  / kMuseTypicalCubeSizeY * aScale; /* [cm] */
328  areas[istack] = cpl_table_get_column_mean(stack, MUSE_GEOTABLE_WIDTH)
329  * height * nperstack /* summed widths [pix] */
330  / kMuseTypicalCubeSizeX * aScale; /* [cm] */
331  cpl_table_delete(stack);
332 #if 0
333  cpl_msg_debug(__func__, "areas[%d] = %f", istack, areas[istack]);
334 #endif
335  area += areas[istack];
336  } /* for istack (all slicer stacks) */
337  cpl_table_delete(table);
338  return area;
339 } /* muse_geo_table_ifu_area() */
340 
341 /*----------------------------------------------------------------------------*/
352 /*----------------------------------------------------------------------------*/
353 cpl_vector *
354 muse_geo_lines_get(const cpl_table *aLines)
355 {
356  cpl_ensure(aLines, CPL_ERROR_NULL_INPUT, NULL);
357 
358  /* duplicate the table, so that we can do nasty things to it */
359  cpl_table *tlines = cpl_table_duplicate(aLines);
360  /* cast all floating-point columns to double so that we can use the double *
361  * functions without getting errors (in case they are just float) */
362  cpl_table_cast_column(tlines, MUSE_LINE_CATALOG_LAMBDA, MUSE_LINE_CATALOG_LAMBDA,
363  CPL_TYPE_DOUBLE);
364  cpl_table_cast_column(tlines, MUSE_LINE_CATALOG_FLUX, MUSE_LINE_CATALOG_FLUX,
365  CPL_TYPE_DOUBLE);
366  cpl_table_unselect_all(tlines);
367 
368  /* select all lines we really don't want, because they are *
369  * either of the wrong lamp (Xe is not used for geometrical *
370  * exposures), have too little flux, are below the MUSE *
371  * wavelength range, or have for other reasons quality. */
372  cpl_table_or_selected_string(tlines, MUSE_LINE_CATALOG_ION, CPL_EQUAL_TO, "XeI");
373  cpl_table_or_selected_double(tlines, MUSE_LINE_CATALOG_FLUX, CPL_LESS_THAN, 5000.);
374  cpl_table_or_selected_double(tlines, MUSE_LINE_CATALOG_LAMBDA, CPL_LESS_THAN,
375  kMuseNominalLambdaMin);
376  cpl_table_or_selected_int(tlines, MUSE_LINE_CATALOG_QUALITY, CPL_LESS_THAN, 1);
377  cpl_table_erase_selected(tlines);
378 
379  /* We have enough Neon lines, so remove those with low quality or *
380  * low flux, unless they happen to be the last in the remaining table *
381  * (since we want to cover as wide a wavelength range as possible). */
382  cpl_table_or_selected_string(tlines, MUSE_LINE_CATALOG_ION, CPL_EQUAL_TO, "NeI");
383  cpl_table_and_selected_int(tlines, MUSE_LINE_CATALOG_QUALITY, CPL_LESS_THAN, 2);
384  cpl_table_unselect_row(tlines, cpl_table_get_nrow(tlines) - 1);
385  cpl_table_erase_selected(tlines);
386  cpl_table_or_selected_string(tlines, MUSE_LINE_CATALOG_ION, CPL_EQUAL_TO, "NeI");
387  cpl_table_and_selected_double(tlines, MUSE_LINE_CATALOG_FLUX, CPL_LESS_THAN, 10000.);
388  cpl_table_unselect_row(tlines, cpl_table_get_nrow(tlines) - 1);
389  cpl_table_erase_selected(tlines);
390 
391  /* now the selection should be done, just check the number *
392  * before converting the "lambda" column into a vector */
393  int nlines = cpl_table_get_nrow(tlines);
394  if (nlines <= 5) {
395  cpl_table_delete(tlines);
396  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
397  "Only found %d suitable arc lines!", nlines);
398  return NULL;
399  }
400  cpl_vector *lines = cpl_vector_wrap(nlines,
401  cpl_table_unwrap(tlines, MUSE_LINE_CATALOG_LAMBDA));
402  cpl_table_delete(tlines);
403  cpl_msg_info(__func__, "Using a list of %d arc lines (from %.1f to %.1f "
404  "Angstrom)", nlines, cpl_vector_get(lines, 0),
405  cpl_vector_get(lines, nlines - 1));
406  return lines;
407 } /* muse_geo_lines_get() */
408 
409 /*----------------------------------------------------------------------------*/
456 /*----------------------------------------------------------------------------*/
457 cpl_table *
459  const cpl_table *aTrace, const cpl_table *aWave,
460  const cpl_vector *aLines, double aSigma,
461  muse_geo_centroid_type aCentroid)
462 {
463  cpl_ensure(aImage && aList && aTrace && aWave && aLines, CPL_ERROR_NULL_INPUT,
464  NULL);
465  cpl_ensure(aSigma > 0., CPL_ERROR_ILLEGAL_INPUT, NULL);
466  unsigned int nimages = muse_imagelist_get_size(aList);
467  cpl_ensure(nimages >= 5, CPL_ERROR_ILLEGAL_INPUT, NULL);
468  int nlines = cpl_vector_get_size(aLines);
469  cpl_ensure(nlines >= 3, CPL_ERROR_ILLEGAL_INPUT, NULL);
470  cpl_ensure(aCentroid <= MUSE_GEO_CENTROID_GAUSSIAN, CPL_ERROR_ILLEGAL_INPUT,
471  NULL);
472 
473  int ny = cpl_image_get_size_y(aImage->data),
474  nentries = kMuseSlicesPerCCD * kMuseCUmpmSpotsPerSlice * nlines * nimages;
475  cpl_table *measurements = muse_cpltable_new(muse_geo_measurements_def,
476  nentries);
477  const unsigned char ifu = muse_utils_get_ifu(aImage->header);
478  int iline, irow = 0;
479  for (iline = 0; iline < nlines; iline++) {
480  double lambda = cpl_vector_get(aLines, iline);
481  cpl_msg_info(__func__, "Searching for line %d (%.3f Angstrom) in IFU %2hhu",
482  iline + 1, lambda, ifu);
483 
484  unsigned short nslice;
485  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
486  cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
487  nslice),
488  *pwave = muse_wave_table_get_poly_for_slice(aWave, nslice);
489  if (!ptrace || !pwave) {
490  muse_trace_polys_delete(ptrace);
491  cpl_polynomial_delete(pwave);
492  continue;
493  }
494  double xc = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ny / 2, NULL);
495  /* extract a 1D polynomial at the approximate slice center, *
496  * since evaluating that is faster than the one in 2D */
497  cpl_polynomial *pxconst = cpl_polynomial_new(1);
498  cpl_size p = 0;
499  cpl_polynomial_set_coeff(pxconst, &p, xc);
500  cpl_polynomial *pywave = cpl_polynomial_extract(pwave, 0, pxconst);
501  cpl_polynomial_delete(pxconst);
502  /* search for y-position where the wavelength approximately matches */
503  double yc = 1, lbda = -1;
504  while (fabs(lambda - lbda) > 1.) {
505  lbda = cpl_polynomial_eval_1d(pywave, yc, NULL);
506  yc += 0.5; /* better do small steps */
507  if (yc > kMuseOutputYTop) { /* safeguard against infinite loop */
508  break;
509  }
510  } /* while */
511  cpl_polynomial_delete(pywave);
512 #if 0
513  cpl_msg_debug(__func__, "--> %.3f --> %f,%f in slice %2hu",
514  lbda, xc, yc, nslice);
515 #endif
516  cpl_polynomial_delete(pwave);
517  /* if the difference is still large, then the polynomial *
518  * was faulty, so warn and continue with the next slice */
519  if (fabs(lambda - lbda) > 1.) {
520  cpl_msg_warning(__func__, "Polynomial in slice %2hu of IFU %2hhu appears"
521  " to be faulty! Skipping measurement of line %d (%.1f "
522  "Angstrom)", nslice, ifu, iline + 1, lambda);
523  muse_trace_polys_delete(ptrace);
524  continue;
525  }
526 
527  /* we are ~0.5 pix to high, but that should be fine on average, *
528  * because we now extract a box that should be large enough */
529 #define DETECTION_HALFSIZE 7
530  int xl = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], yc, NULL)),
531  xr = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], yc, NULL)),
532  yb = lround(yc - DETECTION_HALFSIZE),
533  yt = lround(yc + DETECTION_HALFSIZE);
534  cpl_image *box = cpl_image_extract(aImage->data, xl, yb, xr, yt);
535  /* smooth the image with a Gauss-filter before creating detection mask */
536  cpl_image *fbox = cpl_image_duplicate(box);
537  cpl_matrix *gkernel = muse_matrix_new_gaussian_2d(2, 2, 1.);
538  cpl_image_filter(fbox, box, gkernel, CPL_FILTER_LINEAR, CPL_BORDER_FILTER);
539  cpl_matrix_delete(gkernel);
540  cpl_stats_mode mode = CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV;
541  cpl_stats *s = cpl_stats_new_from_image(box, mode);
542  double limit = cpl_stats_get_median(s)
543  + aSigma * cpl_stats_get_median_dev(s);
544  cpl_mask *mask = cpl_mask_threshold_image_create(fbox, limit, DBL_MAX);
545 #if 0 /* file debug output for spot detection */
546  char *fn1 = cpl_sprintf("box_%02hu.fits", nslice),
547  *fn2 = cpl_sprintf("boxf_%02hu.fits", nslice),
548  *fn3 = cpl_sprintf("boxf_%02hu_mask.fits", nslice);
549  cpl_image_save(box, fn1, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
550  cpl_image_save(fbox, fn2, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
551  cpl_mask_save(mask, fn3, NULL, CPL_IO_CREATE);
552  cpl_free(fn1);
553  cpl_free(fn2);
554  cpl_free(fn3);
555 #endif
556  /* extract the apertures on the original unsmoothed image */
557  cpl_apertures *apertures = cpl_apertures_extract_mask(box, mask);
558 #if 0 /* debugging of box, statistics, and apertures */
559  cpl_msg_debug(__func__, "stats in box [%d:%d,%d:%d] --> limit = "
560  "%f + %.1f * %f = %f, apertures:", xl, xr, yb, yt,
561  cpl_stats_get_median(s), aSigma,
562  cpl_stats_get_median_dev(s), limit);
563  cpl_apertures_dump(apertures, stdout);
564  fflush(stdout);
565 #endif
566  cpl_mask_delete(mask);
567  cpl_stats_delete(s);
568  cpl_image_delete(fbox);
569  cpl_image_delete(box);
570  cpl_errorstate es = cpl_errorstate_get();
571  int nspots = cpl_apertures_get_size(apertures);
572  if (nspots < 0) {
573  nspots = 0;
574  }
575  if (!apertures || nspots != kMuseCUmpmSpotsPerSlice) {
576  cpl_msg_debug(__func__, "found %d spot%s (need %hhu) down to the %.1f"
577  "-sigma limit in slice %2d for wavelength %.3f Angstrom "
578  "in box [%d:%d,%d:%d]", nspots, nspots == 1 ? "" : "s",
579  kMuseCUmpmSpotsPerSlice, aSigma, nslice, lambda,
580  xl, xr, yb, yt);
581  muse_trace_polys_delete(ptrace);
582  cpl_apertures_delete(apertures);
583  cpl_errorstate_set(es);
584  continue;
585  }
586 #if 0
587  cpl_msg_debug(__func__, "found %d spots using the %.1f-sigma limit in "
588  "slice %2d for wavelength %.3f Angstrom in box [%d:%d,%d:%d]",
589  nspots, aSigma, nslice, lambda, xl, xr, yb, yt);
590  cpl_apertures_dump(apertures, stdout);
591  fflush(stdout);
592 #endif
593  /* Make sure that the spots are ordered in increasing x-pixel position. *
594  * Since the apertures cannot be sorted by that, use a temporary matrix *
595  * to sort and an index vector to store the sorted aperture numbers. */
596  cpl_matrix *mspots = cpl_matrix_new(nspots, 2);
597  int naper;
598  for (naper = 1; naper <= nspots; naper++) {
599  double xpos = cpl_apertures_get_centroid_x(apertures, naper);
600  cpl_matrix_set(mspots, naper - 1, 0, xpos);
601  cpl_matrix_set(mspots, naper - 1, 1, naper);
602  } /* for naper (all spot apertures) */
603  cpl_matrix_sort_rows(mspots, 1); /* this sorts by decreasing x-position! */
604 #if 0
605  printf("mspots sorted:\n");
606  cpl_matrix_dump(mspots, stdout);
607  fflush(stdout);
608 #endif
609  /* now create index buffer, with reverse order */
610  int idx, *aperidx = cpl_calloc(nspots, sizeof(int));
611  for (naper = 1, idx = nspots - 1; naper <= nspots && idx >= 0; naper++, idx--) {
612  /* save aperture number at correct index position */
613  aperidx[naper - 1] = cpl_matrix_get(mspots, idx, 1);
614  } /* for naper / i */
615  cpl_matrix_delete(mspots);
616 
617  /* now measure the three spots in each of the images of the list *
618  * and record the spot properties as well as those of the image */
619  unsigned int k;
620  for (k = 0; k < nimages; k++) {
621  muse_image *image = muse_imagelist_get(aList, k);
622  int posenc[4];
623  double pospos[4],
624  posang = muse_astro_posangle(image->header);
625  unsigned short i;
626  for (i = 1; i <= 4; i++) {
627  posenc[i-1] = muse_pfits_get_posenc(image->header, i);
628  pospos[i-1] = muse_pfits_get_pospos(image->header, i);
629  }
630 
631 #define MEASUREMENT_HALFSIZE 5
632 #define BACKGROUND_HALFSIZE 7
633  for (idx = 0; idx < nspots; idx++) {
634  naper = aperidx[idx];
635  double xpos = cpl_apertures_get_centroid_x(apertures, naper) + xl,
636  ypos = cpl_apertures_get_centroid_y(apertures, naper) + yb;
637 #if 0
638  printf("aper %d idx %d xpos %f\n", naper, idx, xpos);
639  fflush(stdout);
640 #endif
641  cpl_stats_mode mspot = CPL_STATS_FLUX | CPL_STATS_CENTROID,
642  mbg = CPL_STATS_MEAN;
643  cpl_stats *sspot = cpl_stats_new_from_image_window(image->data, mspot,
644  xpos - MEASUREMENT_HALFSIZE,
645  ypos - MEASUREMENT_HALFSIZE,
646  xpos + MEASUREMENT_HALFSIZE,
647  ypos + MEASUREMENT_HALFSIZE),
648  *sbg = cpl_stats_new_from_image_window(image->data, mbg,
649  xpos - BACKGROUND_HALFSIZE,
650  ypos - BACKGROUND_HALFSIZE,
651  xpos + BACKGROUND_HALFSIZE,
652  ypos + BACKGROUND_HALFSIZE);
653  int npix = cpl_stats_get_npix(sspot);
654  double bg = cpl_stats_get_mean(sbg),
655  flux = cpl_stats_get_flux(sspot) - bg * npix;
656  if (flux < 0.) {
657  flux = 0.;
658  }
659  cpl_stats_delete(sbg);
660  double xcentroid = cpl_stats_get_centroid_x(sspot),
661  ycentroid = cpl_stats_get_centroid_y(sspot);
662  cpl_stats_delete(sspot);
663  double xfwhm, yfwhm;
664  if (aCentroid == MUSE_GEO_CENTROID_GAUSSIAN) {
665  /* try a Gaussian fit to derive a better centroid position */
666  cpl_array *gpars = cpl_array_new(7, CPL_TYPE_DOUBLE);
667  cpl_array_set(gpars, 0, bg);
668  cpl_array_set(gpars, 1, flux);
669  cpl_array_set(gpars, 3, xcentroid);
670  cpl_array_set(gpars, 4, ycentroid);
671  cpl_array_set(gpars, 5, 2.); /* assume just critical sampling */
672  cpl_array_set(gpars, 6, 2.);
673  cpl_fit_image_gaussian(image->data, NULL, lround(xcentroid), lround(ycentroid),
674  2 * MEASUREMENT_HALFSIZE + 1, 2 * MEASUREMENT_HALFSIZE + 1,
675  gpars, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
676  xcentroid = cpl_array_get(gpars, 3, NULL);
677  ycentroid = cpl_array_get(gpars, 4, NULL);
678  xfwhm = cpl_array_get(gpars, 5, NULL) * CPL_MATH_FWHM_SIG;
679  yfwhm = cpl_array_get(gpars, 6, NULL) * CPL_MATH_FWHM_SIG;
680  cpl_array_delete(gpars);
681  } else {
682  cpl_image_get_fwhm(image->data, lround(xcentroid), lround(ycentroid),
683  &xfwhm, &yfwhm);
684  }
685  if (cpl_propertylist_has(image->header, MUSE_HDR_TMP_FN)) {
686  cpl_table_set_string(measurements, "filename", irow,
687  cpl_propertylist_get_string(image->header,
688  MUSE_HDR_TMP_FN));
689  } else {
690  cpl_table_set_string(measurements, "filename", irow, "unknown");
691  }
692  cpl_table_set_int(measurements, "image", irow, k + 1);
693  cpl_table_set_int(measurements, "POSENC2", irow, posenc[1]);
694  cpl_table_set(measurements, "POSPOS2", irow, pospos[1]);
695  cpl_table_set_int(measurements, "POSENC3", irow, posenc[2]);
696  cpl_table_set(measurements, "POSPOS3", irow, pospos[2]);
697  cpl_table_set_int(measurements, "POSENC4", irow, posenc[3]);
698  cpl_table_set(measurements, "POSPOS4", irow, pospos[3]);
699  /* compute the "real" vertical position of the mask, *
700  * as VPOS = POS3.POS / sin(POSANG) */
701  cpl_table_set(measurements, "VPOS", irow,
702  pospos[2] / sin(posang * CPL_MATH_RAD_DEG)); // XXX this is total rubbish, it will crash if POSANG==0!
703  cpl_table_set_double(measurements, "ScaleFOV", irow,
705  cpl_table_set_int(measurements, "SubField", irow, ifu);
706  cpl_table_set_int(measurements, "SliceCCD", irow, nslice);
707  cpl_table_set(measurements, "lambda", irow, lambda);
708  cpl_table_set_int(measurements, "SpotNo", irow, idx + 1);
709  cpl_table_set(measurements, "xc", irow, xcentroid);
710  cpl_table_set(measurements, "yc", irow, ycentroid);
711  cpl_table_set(measurements, "xfwhm", irow, xfwhm);
712  cpl_table_set(measurements, "yfwhm", irow, yfwhm);
713  cpl_table_set(measurements, "flux", irow, flux);
714  cpl_table_set(measurements, "bg", irow, bg);
715  /* also compute the relative position towards the slice center [pix] */
716  double xcslice = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ycentroid, NULL);
717  cpl_table_set(measurements, "dxcen", irow, xcentroid - xcslice);
718  /* and the slice width at this CCD position as traced [pix] */
719  double twidth = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], ycentroid, NULL)
720  - cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], ycentroid, NULL);
721  cpl_table_set(measurements, "twidth", irow, twidth);
722 #if 0 /* exclusion of cosmic rays or other rubbish by detection of weird *
723  * FWHM values did not work at all but excluded lots of valid points */
724  if (xfwhm < 0 || yfwhm < 0) {
725  cpl_msg_warning(__func__, "xfwhm and/or yfwhm are invalid: %f, %f:", xfwhm, yfwhm);
726  if (xfwhm < 0) {
727  cpl_table_set_invalid(measurements, "xfwhm", irow);
728  }
729  if (yfwhm < 0) {
730  cpl_table_set_invalid(measurements, "yfwhm", irow);
731  }
732  cpl_table_dump(measurements, irow, 1, stdout);
733  fflush(stdout);
734  }
735 #endif
736  irow++;
737  } /* for naper (all spot apertures) */
738  } /* for k (all images in list) */
739  cpl_apertures_delete(apertures);
740  cpl_free(aperidx);
741  muse_trace_polys_delete(ptrace);
742  } /* for nslice */
743  } /* for iline */
744 
745  /* clean up unused rows */
746  cpl_table_erase_invalid(measurements);
747 
748  /* sort table to get spots close together */
749  cpl_propertylist *order = cpl_propertylist_new();
750  cpl_propertylist_append_bool(order, "lambda", CPL_FALSE);
751  cpl_propertylist_append_bool(order, "SliceCCD", CPL_FALSE);
752  cpl_propertylist_append_bool(order, "SpotNo", CPL_FALSE);
753  cpl_propertylist_append_bool(order, "VPOS", CPL_FALSE);
754  cpl_table_sort(measurements, order);
755  cpl_propertylist_delete(order);
756 
757  return measurements;
758 } /* muse_geo_measure_spots() */
759 
760 /*----------------------------------------------------------------------------*/
796 /*----------------------------------------------------------------------------*/
797 static cpl_table *
798 muse_geo_get_spot_peaks(cpl_table *aSpots, unsigned char aIFU,
799  unsigned short aNSlice, unsigned char aNSpot,
800  double aLambda, double aVPosRef, cpl_boolean aVerifyDY,
801  cpl_array *aDY)
802 {
803  if (!aSpots) { /* return without raising an error */
804  return NULL;
805  }
806 
807  /* This is not very efficient (one could use the sortedness of the *
808  * table for a more clever selection algorithm) but no bottleneck, *
809  * so leave it like this for the moment. */
810  cpl_table_unselect_all(aSpots);
811  cpl_size irow, nrow = cpl_table_get_nrow(aSpots);
812  for (irow = 0; irow < nrow; irow++) {
813  if (cpl_table_get_int(aSpots, "SliceCCD", irow, NULL) == aNSlice &&
814  cpl_table_get_int(aSpots, "SpotNo", irow, NULL) == aNSpot &&
815  cpl_table_get_double(aSpots, "lambda", irow, NULL) == aLambda) {
816  cpl_table_select_row(aSpots, irow);
817  }
818  } /* for irow */
819  cpl_size nextracted = cpl_table_count_selected(aSpots);
820  if (nextracted < 1) { /* no entries for this combination */
821  /* this means that we cannot do the analysis for this slice at *
822  * this wavelength at all, skip the rest of the spots, too */
823  cpl_msg_debug(__func__, "No detection for spot %1hhu in slice %2hu of IFU "
824  "%hhu at wavelength %.3f", aNSpot, aNSlice, aIFU, aLambda);
825  return NULL;
826  }
827  cpl_table *tspot = cpl_table_extract_selected(aSpots);
828 #if 0
829  printf("tspot:\n");
830  cpl_table_dump(tspot, 0, 10000, stdout);
831  fflush(stdout);
832 #endif
833  /* convert the "flux" table column into an image and detect peaks */
834  int nsrow = cpl_table_get_nrow(tspot);
835  cpl_image *imflux = cpl_image_wrap(nsrow, 1,
836  CPL_TYPE_DOUBLE,
837  cpl_table_get_data_double(tspot, "flux"));
838  /* create a mask of the fiducial peaks and filter (dilate) *
839  * the mask to make sure to include enough of the flux */
840  cpl_stats *s = cpl_stats_new_from_image(imflux,
841  CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV);
842  double limit = cpl_stats_get_median(s) + cpl_stats_get_median_dev(s) * 0.5;
843  cpl_stats_delete(s);
844  if (limit > 500.) {
845  limit = 500.;
846  }
847  cpl_mask *mask = cpl_mask_threshold_image_create(imflux, limit, DBL_MAX);
848  cpl_mask *kernel = cpl_mask_new(3, 1);
849  cpl_mask_not(kernel);
850  cpl_mask *mask2 = cpl_mask_duplicate(mask);
851  cpl_mask_filter(mask, mask2, kernel, CPL_FILTER_DILATION, CPL_BORDER_NOP);
852  cpl_mask_delete(mask2);
853  cpl_mask_delete(kernel);
854  cpl_apertures *aper = cpl_apertures_extract_mask(imflux, mask);
855  cpl_mask_delete(mask);
856  if (!aper) {
857  cpl_msg_info(__func__, "No detection for spot %1hhu in slice %2hu of IFU "
858  "%2hhu at wavelength %.3f (hope for other wavelengths!)",
859  aNSpot, aNSlice, aIFU, aLambda);
860  cpl_table_delete(tspot);
861  cpl_image_unwrap(imflux);
862  return NULL; /* no analysis possible, skip spots in this slice at this wavelength */
863  }
864  /* take aperture closest to the image center, but only if it's not on the border */
865  double dcenter = DBL_MAX;
866  int naper, ndcenter = -1;
867  for (naper = 1; naper <= cpl_apertures_get_size(aper); naper++) {
868  /* exclude apertures with too few positions */
869  int npos = cpl_apertures_get_npix(aper, naper);
870  if (cpl_apertures_get_size(aper) > 1 && npos < 3) {
871  cpl_msg_debug(__func__, "IFU %2hhu SliceCCD %2d spot %1hhu lambda %.3f, "
872  "aperture %d: only %d positions -> skip", aIFU, aNSlice,
873  aNSpot, aLambda, naper, npos);
874  continue;
875  }
876  /* start x-reference at the VPOS values of the middle table row */
877  double xref = aVPosRef > 0 ? aVPosRef
878  : cpl_table_get_double(tspot, "VPOS", (nsrow + 1) / 2, NULL),
879  xcentroid = cpl_apertures_get_centroid_x(aper, naper);
880  /* interpolate the corresponding vpos value of this xcentroid, *
881  * take into account that there might be missing entries! */
882  irow = 0;
883  while (++irow + 1 < xcentroid) ;
884  double pp1 = cpl_table_get_double(tspot, "VPOS", irow - 1, NULL),
885  pp2 = cpl_table_get_double(tspot, "VPOS", irow, NULL),
886  ppfrac = xcentroid - irow;
887 #if 0
888  cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" (%f) --> %f %f ==> %f", irow,
889  xcentroid, pp1, pp2, pp1 * (1 - ppfrac) + pp2 * ppfrac);
890 #endif
891  double ppcentroid = pp1 * (1 - ppfrac) + pp2 * ppfrac;
892  /* now computed distance to the center of the previous spot */
893  double dc = fabs(ppcentroid - xref);
894  int x1 = cpl_apertures_get_left(aper, naper),
895  x2 = cpl_apertures_get_right(aper, naper);
896  if (dc < dcenter && x1 > 1 && x2 < nsrow) {
897  dcenter = dc;
898  ndcenter = naper;
899  } /* if */
900  } /* for naper */
901 
902  /* derive the vertical pinhole distance (which only nominally is 0.6135 mm) */
903  if (aDY || aVerifyDY) {
904  for (naper = 1; naper < cpl_apertures_get_size(aper); naper++) {
905  int l1 = cpl_apertures_get_left(aper, naper),
906  r1 = cpl_apertures_get_right(aper, naper),
907  l2 = cpl_apertures_get_left(aper, naper + 1),
908  r2 = cpl_apertures_get_right(aper, naper + 1);
909  if (l1 > 1 && r1 < nsrow && l2 > 1 && r2 < nsrow) {
910  /* compute peak centroids for both selected apertures */
911  double peak[2];
912  int n2aper;
913  for (n2aper = naper; n2aper <= naper + 1; n2aper++) {
914  cpl_size irow1 = cpl_apertures_get_left(aper, n2aper) - 1,
915  irow2 = cpl_apertures_get_right(aper, n2aper) - 1;
916  double vpos = 0., ftot = 0.;
917  for (irow = irow1; irow <= irow2; irow++) {
918  double flux = cpl_table_get(tspot, "flux", irow, NULL);
919  ftot += flux;
920  vpos += cpl_table_get(tspot, "VPOS", irow, NULL) * flux;
921  } /* for irow */
922  peak[n2aper - naper] = vpos / ftot;
923  } /* for n2aper */
924  double xcdiff = fabs(peak[1] - peak[0]);
925  if (aDY) { /* record the peak distance in the in/output array */
926  /* set up index for writing into the aDY array */
927  cpl_errorstate state = cpl_errorstate_get();
928  cpl_size idy = 0, ndy = cpl_array_get_size(aDY);
929  while (cpl_array_is_valid(aDY, idy) > 0) { /* skip all valid entries */
930  idy++;
931  }
932  if (cpl_array_get_size(aDY) <= idy) {
933  cpl_array_set_size(aDY, ndy * 1.5);
934  cpl_errorstate_set(state);
935  }
936 #if 0
937  cpl_msg_debug(__func__, "xcdiff = %f (%f - %f = %f) / index %"CPL_SIZE_FORMAT,
938  xcdiff, peak[1], peak[0], peak[1] - peak[0], idy);
939 #endif
940  cpl_array_set_double(aDY, idy, xcdiff);
941  } /* if aDY */
942  if (aVerifyDY) {
943  printf("\"centroids_d_%f.dat\" u 18:16 t \"d %f (%f %f)\" w lp, \\\n",
944  xcdiff, xcdiff, peak[0], peak[1]);
945  char *fn = cpl_sprintf("centroids_d_%f.dat", xcdiff);
946  FILE *fp = fopen(fn, "w");
947  fprintf(fp, "# good centroids at %f and %f --> d = %f mm\n#", peak[0], peak[1], xcdiff);
948  cpl_table_dump(tspot, 0, 10000, fp);
949  fflush(fp);
950  fclose(fp);
951  cpl_free(fn);
952  } /* if aVerifyDY */
953  } /* if non-border apertures */
954  } /* for naper (all but last apertures) */
955  } /* if dy verification required */
956  if (ndcenter < 1) { /* didn't find a suitable aperture */
957  cpl_msg_info(__func__, "Motion of spot %1hhu in slice %2hu of IFU "
958  "%2hhu at wavelength %.3f did not result in usable "
959  "coverage (hope for other wavelengths!)", aNSpot, aNSlice,
960  aIFU, aLambda);
961  cpl_table_delete(tspot);
962  cpl_apertures_delete(aper);
963  cpl_image_unwrap(imflux);
964  return NULL; /* no analysis possible, skip spots in this slice at this wavelength */
965  }
966  cpl_size irow1 = cpl_apertures_get_left(aper, ndcenter) - 1,
967  irow2 = cpl_apertures_get_right(aper, ndcenter) - 1;
968  cpl_apertures_delete(aper);
969  cpl_image_unwrap(imflux);
970 
971  /* select the relevant table rows*/
972  cpl_table_unselect_all(tspot);
973  for (irow = irow1; irow <= irow2; irow++) {
974  cpl_table_select_row(tspot, irow);
975  } /* for irow */
976  cpl_table *result = cpl_table_extract_selected(tspot);
977  cpl_table_delete(tspot);
978  return result;
979 } /* muse_geo_get_spot_peaks() */
980 
981 /*----------------------------------------------------------------------------*/
1014 /*----------------------------------------------------------------------------*/
1015 cpl_error_code
1016 muse_geo_compute_pinhole_local_distance(cpl_array *aDY, cpl_table *aSpots)
1017 {
1018  cpl_ensure_code(aDY && aSpots, CPL_ERROR_NULL_INPUT);
1019  cpl_ensure_code(cpl_array_get_type(aDY) == CPL_TYPE_DOUBLE,
1020  CPL_ERROR_INCOMPATIBLE_INPUT);
1021  cpl_size nrow = cpl_table_get_nrow(aSpots);
1022  cpl_ensure_code(nrow > 10, CPL_ERROR_ILLEGAL_INPUT);
1023  cpl_ensure_code(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
1024  CPL_ERROR_INCOMPATIBLE_INPUT);
1025  const unsigned char ifu = cpl_table_get_column_min(aSpots, "SubField"),
1026  ifu2 = cpl_table_get_column_max(aSpots, "SubField");
1027  cpl_ensure_code(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1028  CPL_ERROR_ILLEGAL_INPUT);
1029  cpl_ensure_code(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
1030  CPL_ERROR_ILLEGAL_INPUT);
1031 
1032  cpl_boolean verifydy = getenv("MUSE_DEBUG_GEO_VERIFY_DY")
1033  && atoi(getenv("MUSE_DEBUG_GEO_VERIFY_DY")) > 0;
1034  if (verifydy) {
1035  cpl_msg_warning(__func__, "Running with DY pinhole distance verification on"
1036  " (MUSE_DEBUG_GEO_VERIFY_DY=%s), will produce lots of files "
1037  "\"centroids_d_*.dat\"!", getenv("MUSE_DEBUG_GEO_VERIFY_DY"));
1038  }
1039 
1040  /* extract a list of unique wavelengths from the spots table */
1041  double *lbda = cpl_table_get_data_double(aSpots, "lambda");
1042  cpl_vector *vlbda = cpl_vector_wrap(nrow, lbda);
1043  cpl_vector *lambdas = muse_cplvector_get_unique(vlbda);
1044  cpl_vector_unwrap(vlbda);
1045  int nlines = cpl_vector_get_size(lambdas);
1046 
1047  /* create array with likely size that may be needed *
1048  * (it will be enlarged or or trimmed as required) */
1049  cpl_array *dy = cpl_array_new(kMuseSlicesPerCCD * nlines * kMuseCUmpmSpotsPerSlice,
1050  CPL_TYPE_DOUBLE);
1051 
1052  /* loop through the table, do statistics on each spot *
1053  * and record the vertical pinhole position difference *
1054  * for each doubly illuminated slice */
1055  unsigned short nslice;
1056  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1057  int iline;
1058  for (iline = 0; iline < nlines; iline++) {
1059  double lambda = cpl_vector_get(lambdas, iline);
1060 
1061  unsigned char nspot;
1062  double vposref = -DBL_MAX;
1063  for (nspot = 1; nspot <= kMuseCUmpmSpotsPerSlice; nspot++) {
1064  cpl_table *tspot = muse_geo_get_spot_peaks(aSpots, ifu, nslice, nspot,
1065  lambda, vposref, verifydy, dy);
1066  cpl_table_delete(tspot);
1067  } /* for nspot (all spots) */
1068  } /* for iline (all wavelengths) */
1069  } /* for nslice (all slices) */
1070  cpl_vector_delete(lambdas);
1071  /* now resize the array to the real size needed and append it to the input array */
1073  cpl_msg_debug(__func__, "Median vertical pinhole distance in IFU %02hhu: %f mm",
1074  ifu, cpl_array_get_median(dy));
1075  #pragma omp critical (geo_dy_array_insert)
1076  cpl_array_insert(aDY, dy, cpl_array_get_size(aDY));
1077  cpl_array_delete(dy);
1078 
1079  return CPL_ERROR_NONE;
1080 } /* muse_geo_compute_pinhole_local_distance() */
1081 
1082 /*----------------------------------------------------------------------------*/
1110 /*----------------------------------------------------------------------------*/
1111 double
1112 muse_geo_compute_pinhole_global_distance(cpl_array *aDY, double aWidth,
1113  double aMin, double aMax)
1114 {
1115  cpl_ensure(aDY, CPL_ERROR_NULL_INPUT, 0.);
1116  cpl_ensure(cpl_array_get_type(aDY) == CPL_TYPE_DOUBLE,
1117  CPL_ERROR_INCOMPATIBLE_INPUT, 0.);
1118  cpl_ensure(cpl_array_count_invalid(aDY) < cpl_array_get_size(aDY),
1119  CPL_ERROR_ILLEGAL_INPUT, 0.);
1120 
1121  /* create a histogram and clean it */
1122  cpl_bivector *histogram = muse_cplarray_histogram(aDY, aWidth, aMin, aMax);
1123 #if 0
1124  printf("aDY array 1: %f +/- %f (%f)\n", cpl_array_get_mean(aDY),
1125  cpl_array_get_stdev(aDY), cpl_array_get_median(aDY));
1126  cpl_array_dump(aDY, 0, 1000000, stdout);
1127  printf("aDY histogram 1:\n");
1128  cpl_plot_bivector(NULL, "w lp", NULL, histogram);
1129  cpl_bivector_dump(histogram, stdout);
1130  fflush(stdout);
1131 #endif
1132  muse_cplarray_erase_outliers(aDY, histogram, 1, 0.5);
1133  cpl_bivector_delete(histogram);
1134  double mean = cpl_array_get_mean(aDY),
1135  stdev = cpl_array_get_stdev(aDY),
1136  min = mean - 2 * stdev,
1137  max = mean + 2 * stdev,
1138  step = (max - min) / 20.;
1139 #if 0
1140  double median = cpl_array_get_median(aDY);
1141  printf("aDY array 2: %f +/- %f (%f)\n", mean, stdev, median);
1142  cpl_array_dump(aDY, 0, 1000000, stdout);
1143  fflush(stdout);
1144 #endif
1145  histogram = muse_cplarray_histogram(aDY, step, min, max);
1146 #if 0
1147  printf("aDY histogram 2:\n");
1148  cpl_plot_bivector(NULL, "w lp", NULL, histogram);
1149  cpl_bivector_dump(histogram, stdout);
1150  fflush(stdout);
1151 #endif
1152  muse_cplarray_erase_outliers(aDY, histogram, 1, 0.5);
1153  cpl_bivector_delete(histogram);
1154  mean = cpl_array_get_mean(aDY);
1155  stdev = cpl_array_get_stdev(aDY);
1156 #if 0
1157  double median2 = cpl_array_get_median(aDY);
1158  printf("aDY array 3: %f +/- %f (%f)\n", mean, stdev, median2);
1159  cpl_array_dump(aDY, 0, 1000000, stdout);
1160  fflush(stdout);
1161 #endif
1162 
1163  /* print output and set the computed value in the environment, *
1164  * if the variable does not already exist */
1165  cpl_msg_info(__func__, "Computed vertical pinhole distance of %.6f +/- %.6f "
1166  "mm (instead of %.4f)", mean, stdev, kMuseCUmpmDY);
1167  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1168  cpl_msg_warning(__func__, "Vertical pinhole distance already overridden in the "
1169  "environment (%f mm)", atof(getenv("MUSE_GEOMETRY_PINHOLE_DY")));
1170  } else {
1171  char *envstring = cpl_sprintf("%f", mean);
1172  int err = setenv("MUSE_GEOMETRY_PINHOLE_DY", envstring, 1);
1173  if (!err) {
1174  cpl_msg_info(__func__, "Set MUSE_GEOMETRY_PINHOLE_DY=%s in the environment",
1175  envstring);
1176  }
1177  cpl_free(envstring);
1178  }
1179 
1180  return mean;
1181 } /* muse_geo_compute_pinhole_global_distance() */
1182 
1183 /*----------------------------------------------------------------------------*/
1194 /*----------------------------------------------------------------------------*/
1196 muse_geo_table_new(cpl_size aNRows, double aScale)
1197 {
1198  muse_geo_table *gt = cpl_calloc(1, sizeof(muse_geo_table));
1199  gt->table = muse_cpltable_new(muse_geo_table_def, aNRows);
1200  gt->scale = aScale;
1201  return gt;
1202 }
1203 
1204 /*----------------------------------------------------------------------------*/
1215 /*----------------------------------------------------------------------------*/
1218 {
1219  cpl_ensure(aGeo, CPL_ERROR_NULL_INPUT, NULL);
1220  muse_geo_table *gt = cpl_calloc(1, sizeof(muse_geo_table));
1221  gt->table = cpl_table_duplicate(aGeo->table);
1222  gt->scale = aGeo->scale;
1223  return gt;
1224 }
1225 
1226 /*----------------------------------------------------------------------------*/
1235 /*----------------------------------------------------------------------------*/
1236 void
1238 {
1239  if (!aGeo) {
1240  return;
1241  }
1242  cpl_table_delete(aGeo->table);
1243  aGeo->table = NULL;
1244  cpl_free(aGeo);
1245 }
1246 
1247 /*----------------------------------------------------------------------------*/
1311 /*----------------------------------------------------------------------------*/
1313 muse_geo_determine_initial(cpl_table *aSpots, const cpl_table *aTrace)
1314 {
1315  cpl_ensure(aSpots && aTrace, CPL_ERROR_NULL_INPUT, NULL);
1316  cpl_size nrow = cpl_table_get_nrow(aSpots);
1317  cpl_ensure(nrow > 10, CPL_ERROR_ILLEGAL_INPUT, NULL);
1318  cpl_ensure(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
1319  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
1320  const unsigned char ifu = cpl_table_get_column_min(aSpots, "SubField"),
1321  ifu2 = cpl_table_get_column_max(aSpots, "SubField");
1322  cpl_ensure(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1323  CPL_ERROR_ILLEGAL_INPUT, NULL);
1324  const double kScale = cpl_table_get_column_mean(aSpots, "ScaleFOV");
1325  cpl_ensure(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
1326  CPL_ERROR_ILLEGAL_INPUT, NULL);
1327 
1328  double maskangle = 0., fmaskrot = 1.;
1329  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
1330  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
1331  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
1332  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
1333  "(cos = %.4e)", maskangle, fmaskrot);
1334  }
1335  double pinholedy = kMuseCUmpmDY;
1336  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1337  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
1338  cpl_msg_info(__func__, "Using pinhole y distance of %f mm (instead of "
1339  "%f mm)", pinholedy, kMuseCUmpmDY);
1340  }
1341 
1342  /* extract a list of unique wavelengths from the spots table */
1343  double *lbda = cpl_table_get_data_double(aSpots, "lambda");
1344  cpl_vector *vlbda = cpl_vector_wrap(nrow, lbda);
1345  cpl_vector *lambdas = muse_cplvector_get_unique(vlbda);
1346  cpl_vector_unwrap(vlbda);
1347  int nlines = cpl_vector_get_size(lambdas);
1348  muse_geo_table *gt = muse_geo_table_new(kMuseSlicesPerCCD
1349  * kMuseCUmpmSpotsPerSlice * nlines,
1350  kScale);
1351  /* loop through the table, do statistics on each spot (position vs flux) */
1352  cpl_size irrow = 0; /* row in gt->table */
1353  unsigned short nslice;
1354  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1355  cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
1356  nslice);
1357  if (!ptrace) {
1358  cpl_msg_debug(__func__, "Skipping IFU %2hhu SliceCCD %2d", ifu, nslice);
1359  continue;
1360  }
1361  cpl_msg_info(__func__, "Handling IFU %2hhu SliceCCD %2d", ifu, nslice);
1362  int iline;
1363  for (iline = 0; iline < nlines; iline++) {
1364  double lambda = cpl_vector_get(lambdas, iline);
1365 
1366  unsigned char nspot, nslicespot = 0;
1367  double vposref = -DBL_MAX;
1368  for (nspot = 1; nspot <= kMuseCUmpmSpotsPerSlice; nspot++) {
1369  cpl_table *tspot = muse_geo_get_spot_peaks(aSpots, ifu, nslice, nspot,
1370  lambda, vposref, CPL_FALSE,
1371  NULL);
1372  if (!tspot) {
1373  break;
1374  }
1375  nslicespot++;
1376  double xcenter = 0., ycenter = 0., /* properties for weighted centroids */
1377  vpos = 0., ftot = 0.;
1378  nrow = cpl_table_get_nrow(tspot);
1379  cpl_size irow;
1380  for (irow = 0; irow < nrow; irow++) {
1381  double flux = cpl_table_get(tspot, "flux", irow, NULL);
1382  ftot += flux;
1383  xcenter += cpl_table_get(tspot, "xc", irow, NULL) * flux;
1384  ycenter += cpl_table_get(tspot, "yc", irow, NULL) * flux;
1385  vpos += cpl_table_get(tspot, "VPOS", irow, NULL) * flux;
1386  } /* for irow (tspot table rows) */
1387  cpl_table_delete(tspot);
1388  if (ftot <= 0.) {
1389  cpl_msg_warning(__func__, "Invalid integrated flux of spot %1hhu/%1hhu "
1390  "in slice %2hu of IFU %2hhu at wavelength %.3f: %e",
1391  nspot, nslicespot, nslice, ifu, lambda, ftot);
1392  break;
1393  }
1394  xcenter /= ftot;
1395  ycenter /= ftot;
1396  vpos /= ftot;
1397  if (vposref < 0) {
1398  /* remember the peak center of this spot as reference for the others */
1399  vposref = vpos;
1400  }
1401  double xcen = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ycenter,
1402  NULL),
1403  xl = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], ycenter, NULL),
1404  xr = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], ycenter, NULL),
1405  xwidth = xr - xl;
1406  cpl_msg_debug(__func__, "IFU %2hhu SliceCCD %2d spot %1hhu/%1hhu lambda %.3f "
1407  "x/y %8.3f %8.3f (xcen %8.3f xwidth %6.3f) vpos %f flux %e",
1408  ifu, nslice, nspot, nslicespot, lambda, xcenter, ycenter,
1409  xcen, xwidth, vpos, ftot);
1410  cpl_table_set_int(gt->table, MUSE_GEOTABLE_FIELD, irrow, ifu);
1411  cpl_table_set_int(gt->table, MUSE_GEOTABLE_SKY, irrow,
1412  kMuseGeoSliceSky[nslice - 1]);
1413  cpl_table_set_int(gt->table, MUSE_GEOTABLE_CCD, irrow, nslice);
1414  cpl_table_set_int(gt->table, "spot", irrow, nslicespot);
1415  /* follow the optical numbering, with "reversed" numbering, *
1416  * see VLT-TRE-MUS-14670-0657 v1.06, Sect. 1.4.2.2.3 */
1417  unsigned char stack = nslice <= 12 ? 4 : (nslice <= 24 ? 3 : (nslice <= 36 ? 2 : 1));
1418  cpl_table_set_int(gt->table, "stack", irrow, stack);
1419  /* don't set "spot" column here, see below */
1420  cpl_table_set_double(gt->table, "xc", irrow, xcenter);
1421  cpl_table_set_double(gt->table, "yc", irrow, ycenter);
1422  cpl_table_set_double(gt->table, "dxl", irrow, xcenter - xl);
1423  cpl_table_set_double(gt->table, "dxr", irrow, xr - xcenter);
1424  cpl_table_set_double(gt->table, "vpos", irrow, vpos);
1425  cpl_table_set_double(gt->table, "flux", irrow, ftot);
1426  cpl_table_set_double(gt->table, "lambda", irrow, lambda);
1427  /* set width and angle columns invalid at the beginning, *
1428  * to be able to select invalid table entries */
1429  cpl_table_set_invalid(gt->table, MUSE_GEOTABLE_WIDTH, irrow);
1430  cpl_table_set_invalid(gt->table, MUSE_GEOTABLE_ANGLE, irrow);
1431 
1432  /* use data of all three spots in this slice to compute *
1433  * relative width, angle, and horizontal position */
1434  if (nslicespot == kMuseCUmpmSpotsPerSlice) {
1435  /* get data of the previous two spots back from the table */
1436  int err1a, err2a, err1b, err2b;
1437  double xc1 = cpl_table_get_double(gt->table, "xc", irrow - 2, &err1a),
1438  xc2 = cpl_table_get_double(gt->table, "xc", irrow - 1, &err2a),
1439  vpos1 = cpl_table_get_double(gt->table, "vpos", irrow - 2, &err1b),
1440  vpos2 = cpl_table_get_double(gt->table, "vpos", irrow - 1, &err2b);
1441  if (!err1a && !err2a) {
1442  if (xcenter < xc2 || xc2 < xc1) {
1443  cpl_msg_warning(__func__, "spots are not sorted left-to-right on "
1444  "the CCD (%f .. %f .. %f)!", xc1, xc2, xcenter);
1445  }
1446  double dx = (xcenter - xc1) / 2.;
1447  /* a kind of standard deviation of the three values: *
1448  * dx, dx1, dx2; the term for dx itself is always zero */
1449  double dxerr = sqrt((pow(xcenter - xc2 - dx, 2)
1450  + pow(xc2 - xc1 - dx, 2)) / 3.);
1451  /* See if the error is high, as this means that one of the three *
1452  * positions and hence of the two distances is likely wrong. *
1453  * Then use an estimate of what the distance should really be to *
1454  * decide which of the two distances is correct. */
1455  /* XXX this is ugly, as dxexpected may be a bad estimate, *
1456  * derived using the data of April 2013 with only 6 IFUs */
1457  double xreloffset = 0.;
1458  if (dxerr > 0.3) {
1459  const double dxexpected = 26.04644 - 0.05537208 * ifu;
1460  double dx1 = xc2 - xc1,
1461  dx2 = xcenter - xc2,
1462  diff1 = fabs(dx1 - dxexpected),
1463  diff2 = fabs(dx2 - dxexpected),
1464  dxnew;
1465  if (fmin(diff1, diff2) > 1.5) { /* more than 1.5 pix difference! */
1466  dxnew = 99.; /* set to large value to trigger the bad width case below */
1467  } else if (diff2 > diff1) {
1468  dxnew = dx1;
1469  } else {
1470  dxnew = dx2;
1471  }
1472  /* correct the xrel to be computed below, too, by half *
1473  * the difference in width that this change makes */
1474  xreloffset = xwidth * kMuseCUmpmDX / fmaskrot
1475  * kMuseTypicalCubeSizeX * kScale / 60.
1476  * fabs(1. / dx - 1 / dxnew) / 2.;
1477  cpl_msg_debug(__func__, "IFU %2hhu SliceCCD %2d spot %1hhu/%1hhu "
1478  "lambda %.3f dx %.3f +/- %.3f (%.3f %.3f): dxerr "
1479  "is large, using a guess of %.3f +/- %.3f "
1480  "(expected was %.3f for this IFU), adding %.3f to"
1481  " xrel!", ifu, nslice, nspot, nslicespot, lambda,
1482  dx, dxerr, xc2-xc1, xcenter-xc2, dxnew, dxerr / 2.,
1483  dxexpected, xreloffset);
1484  dx = dxnew;
1485  dxerr /= 2.; /* hopefully we are not a factor of two less wrong! */
1486  } /* if dxerr > 0.3 */
1487  cpl_table_fill_column_window_double(gt->table, "dx", irrow - 2, 3, dx);
1488  cpl_table_fill_column_window_double(gt->table, "dxerr", irrow - 2, 3, dxerr);
1489  /* Project the size back to the focal plane and use the known *
1490  * scales to compute the effective slice width in nominal pixels. *
1491  * Strictly, one should take the slice width from the trace at the *
1492  * center of the three y values, but the difference is negligible. */
1493  double scale = kMuseCUmpmDX / fmaskrot / dx, /* scale of this slice [mm / pix] */
1494  width = xwidth * scale * kMuseTypicalCubeSizeX * kScale / 60.,
1495  werr = width * dxerr / dx; /* error estimate */
1496  if (width > kMuseSliceLoLikelyWidth && width < kMuseSliceHiLikelyWidth) {
1497  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_WIDTH,
1498  irrow - 2, 3, width);
1499  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_WIDTH"err",
1500  irrow - 2, 3, werr);
1501  } else {
1502  cpl_msg_info(__func__, "IFU %2hhu slice %2d lambda %.3f: computed "
1503  "an unlikely width: %.3f +/- %.3f (hope for other"
1504  " wavelengths!)", ifu, nslice, lambda, width, werr);
1505  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_WIDTH,
1506  irrow - 2, 3);
1507  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_WIDTH"err",
1508  irrow - 2, 3);
1509  }
1510 
1511  /* now compute relative horizontal positions against the center *
1512  * of the slice, in mm in the focal plane, for all three spots */
1513  double xrel = (xcen - xc1 + xreloffset) * scale,
1514  xrelerr = fabs(xrel * dxerr / dx);
1515  cpl_table_set_double(gt->table, "xrel", irrow - 2, xrel);
1516  cpl_table_set_double(gt->table, "xrelerr", irrow - 2, xrelerr);
1517  xrel = (xcen - xc2 + xreloffset) * scale;
1518  xrelerr = fabs(xrel * dxerr / dx);
1519  cpl_table_set_double(gt->table, "xrel", irrow - 1, xrel);
1520  cpl_table_set_double(gt->table, "xrelerr", irrow - 1, xrelerr);
1521  xrel = (xcen - xcenter + xreloffset) * scale;
1522  xrelerr = fabs(xrel * dxerr / dx);
1523  cpl_table_set_double(gt->table, "xrel", irrow, xrel);
1524  cpl_table_set_double(gt->table, "xrelerr", irrow, xrelerr);
1525  } /* else: got all x-pos measurements */
1526 
1527  if (!err1b && !err2b) { /* compute angle in degrees */
1528  /* check if the vpos values correspond to the same flux peak or *
1529  * if we need to fix them up for the vertical pinhole distance */
1530  double pdiff = fmax(vpos1, fmax(vpos2, vpos))
1531  - fmin(vpos1, fmin(vpos2, vpos));
1532  if (pdiff > 0.5) { /* max diff. 0.5 arcsec =~ 0.3 mm */
1533  double pmean = (vpos1 + vpos2 + vpos) / 3.;
1534  if (vpos1 > pmean) { /* subtract pinhole distance from high values */
1535  vpos1 -= pinholedy * fmaskrot;
1536  }
1537  if (vpos2 > pmean) {
1538  vpos2 -= pinholedy * fmaskrot;
1539  }
1540  if (vpos > pmean) {
1541  vpos -= pinholedy * fmaskrot;
1542  }
1543  double pdiff2 = fmax(vpos1, fmax(vpos2, vpos))
1544  - fmin(vpos1, fmin(vpos2, vpos));
1545  if (pdiff2 < pdiff) {
1546  cpl_msg_debug(__func__, "Fixed max vpos diff from %f down to %f",
1547  pdiff, pdiff2);
1548  } else { /* didn't work, copy the old values */
1549  double vpos1o = cpl_table_get_double(gt->table, "vpos", irrow - 2, NULL),
1550  vpos2o = cpl_table_get_double(gt->table, "vpos", irrow - 1, NULL),
1551  vpos3o = cpl_table_get_double(gt->table, "vpos", irrow, NULL);
1552  cpl_msg_info(__func__, "Large max vpos diff detected but "
1553  "not fixed! (original: %f %f %f, %f -> mean %f "
1554  "-> fixed: %f %f %f, %f)", vpos1o, vpos2o, vpos3o,
1555  pdiff, pmean, vpos1, vpos2, vpos, pdiff2);
1556  vpos1 = vpos1o;
1557  vpos2 = vpos2o;
1558  vpos = vpos3o;
1559  }
1560  } /* if large vpos differences */
1561 
1562  /* the angles computed here need to be inverted */
1563  double f = 1. / kMuseCUmpmDX / fmaskrot, /* 1 / distance */
1564  angle1 = -atan((vpos2 - vpos1) * f) * CPL_MATH_DEG_RAD,
1565  angle2 = -atan((vpos - vpos2) * f) * CPL_MATH_DEG_RAD,
1566  angle = (angle1 + angle2) / 2.;
1567  /* simple-minded standard deviation again */
1568  double aerr = sqrt((pow(angle1 - angle, 2) + pow(angle2 - angle, 2)) / 3.);
1569  if (fabs(angle) < kMuseGeoSliceMaxAngle) {
1570  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_ANGLE,
1571  irrow - 2, 3, angle);
1572  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_ANGLE"err",
1573  irrow - 2, 3, aerr);
1574  } else {
1575  cpl_msg_info(__func__, "IFU %2hhu slice %2d lambda %.3f: computed"
1576  " an unlikely angle: %.3f +/- %.3f (hope for other "
1577  "wavelengths!)", ifu, nslice, lambda, angle, aerr);
1578  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_ANGLE,
1579  irrow - 2, 3);
1580  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_ANGLE"err",
1581  irrow - 2, 3);
1582  }
1583  } /* else: got all vpos measurements */
1584 
1585  /* and get the dxl value of the left-most spot and the dxr *
1586  * value of the right-most spot and set them for all spots */
1587  double dx = cpl_table_get_double(gt->table, "dx", irrow - 1, NULL),
1588  dxl = cpl_table_get_double(gt->table, "dxl", irrow - 2, NULL),
1589  dxr = cpl_table_get_double(gt->table, "dxr", irrow, NULL);
1590  cpl_table_fill_column_window_double(gt->table, "dxl", irrow - 2, 3, dxl / dx);
1591  cpl_table_fill_column_window_double(gt->table, "dxr", irrow - 2, 3, dxr / dx);
1592  } /* if nslicespot == kMuseCUmpmSpotsPerSlice (last spot in slice) */
1593 
1594  irrow++;
1595  } /* for nspot (all spots) */
1596  } /* for iline (all wavelengths) */
1597  muse_trace_polys_delete(ptrace);
1598  } /* for nslice (all slices) */
1599  cpl_vector_delete(lambdas);
1600  cpl_table_set_size(gt->table, irrow); /* remove empty rows */
1601  /* erase rows which only got a partial analysis (the |break|s above) */
1602  cpl_table_and_selected_invalid(gt->table, MUSE_GEOTABLE_WIDTH);
1603  cpl_table_or_selected_invalid(gt->table, MUSE_GEOTABLE_ANGLE);
1604  cpl_table_erase_selected(gt->table);
1605  return gt;
1606 } /* muse_geo_determine_initial() */
1607 
1608 /*----------------------------------------------------------------------------*/
1628 /*----------------------------------------------------------------------------*/
1629 static cpl_size
1630 muse_geo_determine_horizontal_wmean(const cpl_table *aSlice,
1631  const char *aCol, const char *aColErr,
1632  double *aValue, double *aError,
1633  double *aMedian, double aSigma)
1634 {
1635  const char func[] = "muse_geo_determine_horizontal"; /* pretend to be there */
1636  if (!aSlice) return -1;
1637  if (!aCol || !aValue) return -1;
1638 
1639  /* first create vector(s) from the relevant table columns */
1640  const double *vv = cpl_table_get_data_double_const(aSlice, aCol),
1641  *ve = NULL;
1642  if (aColErr) {
1643  ve = cpl_table_get_data_double_const(aSlice, aColErr);
1644  }
1645  if (!vv) return -1;
1646  /* compute median and median absolute deviation to get starting limits */
1647  cpl_size n = cpl_table_get_nrow(aSlice);
1648  cpl_vector *vtmp = cpl_vector_wrap(n, (double *)cpl_table_get_data_double_const(aSlice, aCol));
1649  double median = cpl_table_get_column_median(aSlice, aCol),
1650  mdev = muse_cplvector_get_adev_const(vtmp, median),
1651  vlo = median - aSigma * mdev,
1652  vhi = median + aSigma * mdev;
1653  cpl_vector_unwrap(vtmp);
1654  cpl_msg_debug(func, "%s/%s: median %f +/- %f --> %f...%f", aCol, aColErr,
1655  median, mdev, vlo, vhi);
1656 
1657  /* iterate: compute weighted mean, using only entries *
1658  * within the computed boundaries */
1659  double value, sigma = mdev,
1660  min, max;
1661  cpl_vector *vmedian = NULL;
1662  cpl_size nrejected;
1663  do {
1664  min = DBL_MAX;
1665  max = -DBL_MAX;
1666  if (aMedian) {
1667  vmedian = cpl_vector_new(n);
1668  }
1669  double weight = 0.;
1670  value = 0.;
1671  nrejected = 0;
1672  cpl_size i, im = 0;
1673  for (i = 0; i < n; i++) {
1674  if (vv[i] < vlo || vv[i] > vhi) { /* skip this entry */
1675  nrejected++;
1676  continue;
1677  }
1678  if (ve) {
1679  value += vv[i] / ve[i];
1680  weight += 1. / ve[i];
1681  } else {
1682  value += vv[i];
1683  weight += 1.;
1684  }
1685  if (vv[i] < min) {
1686  min = vv[i];
1687  }
1688  if (vv[i] > max) {
1689  max = vv[i];
1690  }
1691  if (aMedian) {
1692  cpl_vector_set(vmedian, im++, vv[i]);
1693  }
1694  } /* for i (slice table rows) */
1695  value /= weight;
1696  double wmse = 0.; /* compute weighted MSE */
1697  for (i = 0; i < n; i++) {
1698  if (vv[i] < vlo || vv[i] > vhi) {
1699  continue;
1700  }
1701  if (ve) {
1702  wmse += pow(vv[i] - value, 2) / ve[i];
1703  } else {
1704  wmse += pow(vv[i] - value, 2);
1705  }
1706 #if 0
1707  if (aMedian) {
1708  cpl_msg_debug("vpos", "%f %f %f -> %f", vv[i], vv[i] - value,
1709  pow(vv[i] - value, 2), wmse);
1710  }
1711 #endif
1712  } /* for i (slice table rows) */
1713  wmse /= weight;
1714  if (aMedian) {
1715  cpl_vector_set_size(vmedian, im); /* delete unused entries */
1716  *aMedian = cpl_vector_get_median(vmedian);
1717  cpl_vector_delete(vmedian);
1718  }
1719  /* compute sigma (if we have a valid result) */
1720  if (isnormal(weight) && isfinite(wmse)) {
1721  /* full propagated errors: direct variances plus the weighted MSE */
1722  /* XXX where did this formula originally come from?!? */
1723  sigma = sqrt(ve ? (n - nrejected) / (weight*weight) : 0. + wmse);
1724 #if 0
1725  if (aMedian) {
1726  cpl_msg_debug("vpos", "%f", sigma);
1727  }
1728 #endif
1729  } /* if */
1730  /* compute the new limits (if we weighted any points) */
1731  if (isfinite(value)) {
1732  vlo = value - aSigma * sigma;
1733  vhi = value + aSigma * sigma;
1734  } /* if */
1735 #if 0
1736  cpl_msg_debug(func, "%s/%s: %f +/- %f (+/- %f) --> %f...%f (rej. %"
1737  CPL_SIZE_FORMAT" / %"CPL_SIZE_FORMAT")", aCol, aColErr,
1738  value, sqrt(wmse), sigma, vlo, vhi, nrejected, n);
1739 #endif
1740  } while (nrejected < n && (max > vhi || min < vlo));
1741 
1742  /* last resort, in case all points got rejected: re-use the *
1743  * previous ranges (they were then not overwritten above) and *
1744  * use those as central/final value and their sigma */
1745  if (nrejected == n) {
1746  value = (vlo + vhi) / 2.;
1747  sigma = (vhi - vlo) / (2. * aSigma);
1748 #if 0
1749  cpl_msg_debug(func, "%s/%s: %f +/- %f (ALL rej. %"CPL_SIZE_FORMAT" / %"
1750  CPL_SIZE_FORMAT")", aCol, aColErr, value, sigma, nrejected, n);
1751 #endif
1752  }
1753 
1754  *aValue = value;
1755  if (aError) {
1756  *aError = sigma;
1757  }
1758  return nrejected;
1759 } /* muse_geo_determine_horizontal_wmean() */
1760 
1761 /*----------------------------------------------------------------------------*/
1782 /*----------------------------------------------------------------------------*/
1783 static void
1784 muse_geo_determine_horizontal_vpos(const cpl_table *aSlice,
1785  double *aP, double *aPE, double *aPM,
1786  double aDY, double aF)
1787 {
1788  const char func[] = "muse_geo_determine_horizontal"; /* pretend to be there */
1789  if (!aSlice) return;
1790  if (!aP || !aPE || !aPM) return;
1791 
1792  cpl_vector *vvpos = cpl_vector_wrap(cpl_table_get_nrow(aSlice),
1793  (double *)cpl_table_get_data_double_const(aSlice,
1794  "vpos"));
1795  *aP = cpl_vector_get_mean(vvpos);
1796  *aPE = cpl_vector_get_stdev(vvpos);
1797  *aPM = cpl_vector_get_median_const(vvpos);
1798  /* if the spread is large, then there might be spots *
1799  * from different pinholes involved! */
1800  if (*aPE < 0.01) { /* this should already be good enough */
1801  cpl_vector_unwrap(vvpos);
1802  return;
1803  } /* if */
1804 
1805  cpl_size n = cpl_vector_get_size(vvpos);
1806  cpl_vector *vpp = cpl_vector_duplicate(vvpos),
1807  *vres = cpl_vector_duplicate(vvpos),
1808  *vmedian = cpl_vector_new(n);
1809  cpl_vector_unwrap(vvpos);
1810  cpl_vector_fill(vmedian, *aPM);
1811  cpl_vector_subtract(vres, vmedian);
1812  cpl_vector_delete(vmedian);
1813  double min = cpl_vector_get_min(vres),
1814  max = cpl_vector_get_max(vres);
1815  cpl_msg_debug(func, "vector of vpos values (%.4f +/- %.4f, %.4f) and "
1816  "residuals (%.4f ... %.4f), pinhole distance %.4f",
1817  *aP, *aPE, *aPM, min, max, aDY * aF);
1818 #if 0
1819  cpl_bivector *biv = cpl_bivector_wrap_vectors(vpp, vres);
1820  cpl_bivector_dump(biv, stdout);
1821  fflush(stdout);
1822  cpl_bivector_unwrap_vectors(biv);
1823 #endif
1824  /* Also check the absolute residuals. This case is useful, when *
1825  * half of the value happen to be above and half below the mean, *
1826  * by approximately one pinhole distance -> halfandhalf = true! */
1827  cpl_array *ares = cpl_array_wrap_double(cpl_vector_unwrap(vres), n);
1828  cpl_array_abs(ares);
1829  double amin = cpl_array_get_min(ares),
1830  amax = cpl_array_get_max(ares);
1831  cpl_boolean halfandhalf = fabs(aDY/2. - amin) < 0.01
1832  && fabs(aDY/2. - amax) < 0.01;
1833  cpl_array_delete(ares);
1834 
1835  double p2, pe2, pm2;
1836  cpl_size i;
1837  double limit = fabs(0.9 * aDY * aF);
1838  if (!halfandhalf && /* halfandhalf should be rectified by the else case */
1839  fabs(min) < limit && fabs(max) < limit) {
1840  /* If the largest residuals are less than 90% of the pinhole distance *
1841  * then shifting by one pinhole will not help. Then just compute mean *
1842  * and error, clipping the outliers as for the other properties. */
1843  cpl_size nrej = muse_geo_determine_horizontal_wmean(aSlice, "vpos", NULL,
1844  &p2, &pe2, &pm2, 2.);
1845  cpl_msg_debug(func, "values after rejection vector of vpos values (%.4f "
1846  "+/- %.4f, %.4f), %"CPL_SIZE_FORMAT" rejected", p2, pe2, pm2,
1847  nrej);
1848  } else { /* Largest residual is of the order of the pinhole distance. *
1849  * Try to fix that, by looping through the vector and subtract *
1850  * the pinhole distance from the high values. */
1851  cpl_size nfixed = 0;
1852  for (i = 0; i < n; i++) {
1853  double v = cpl_vector_get(vpp, i);
1854  if (v > *aP) { /* subtract from all values greater than the mean */
1855  cpl_vector_set(vpp, i, v - aDY * aF);
1856  nfixed++;
1857  } /* if */
1858  } /* for i */
1859  p2 = cpl_vector_get_mean(vpp);
1860  pe2 = cpl_vector_get_stdev(vpp);
1861  pm2 = cpl_vector_get_median_const(vpp);
1862  cpl_msg_debug(func, "fixed vector of vpos values (%.4f +/- %.4f, %.4f), "
1863  "%"CPL_SIZE_FORMAT" fixed", p2, pe2, pm2, nfixed);
1864 #if 0
1865  cpl_vector_dump(vpp, stdout);
1866  fflush(stdout);
1867 #endif
1868  } /* else */
1869  cpl_vector_delete(vpp);
1870  if (pe2 < *aPE) { /* if improved, keep these values */
1871  *aP = p2;
1872  *aPE = pe2;
1873  *aPM = pm2;
1874  }
1875 } /* muse_geo_determine_horizontal_vpos() */
1876 
1877 /*----------------------------------------------------------------------------*/
1927 /*----------------------------------------------------------------------------*/
1930 {
1931  cpl_ensure(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT, NULL);
1932  cpl_size nrow = cpl_table_get_nrow(aGeo->table);
1933  cpl_ensure(nrow >= 50, CPL_ERROR_ILLEGAL_INPUT, NULL);
1934  cpl_ensure(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
1935  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
1936  const unsigned char ifu = cpl_table_get_column_min(aGeo->table, MUSE_GEOTABLE_FIELD),
1937  ifu2 = cpl_table_get_column_max(aGeo->table, MUSE_GEOTABLE_FIELD);
1938  cpl_ensure(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1939  CPL_ERROR_ILLEGAL_INPUT, NULL);
1940 
1941  double maskangle = 0., fmaskrot = 1.;
1942  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
1943  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
1944  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
1945  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
1946  "(cos = %.4e)", maskangle, fmaskrot);
1947  }
1948  double pinholedy = kMuseCUmpmDY;
1949  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1950  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
1951  cpl_msg_info(__func__, "Using pinhole y distance of %f mm (instead of "
1952  "%f mm)", pinholedy, kMuseCUmpmDY);
1953  }
1954  cpl_boolean stdgap = getenv("MUSE_GEOMETRY_STD_GAP")
1955  && atoi(getenv("MUSE_GEOMETRY_STD_GAP")) > 0;
1956  if (stdgap) {
1957  cpl_msg_warning(__func__, "Using old (standard) gap computation");
1958  } else {
1959  cpl_msg_info(__func__, "Using new (alternative) gap computation");
1960  }
1961  const double kScaleX = kMuseTypicalCubeSizeX * aGeo->scale / 60.;
1962 
1963  /* select the middle spot, to get per-slice properties */
1965  cpl_table_and_selected_int(gt->table, "spot", CPL_NOT_EQUAL_TO, 2);
1966  cpl_table_erase_selected(gt->table);
1967  /* now results for different wavelengths are adjacent */
1968 #if 0
1969  FILE *f = fopen("bla_horizontal.dat", "w");
1970  fprintf(f, "#");
1971  cpl_table_dump(gt->table, 0, 100000000, f);
1972  fclose(f);
1973  f = fopen("bla_lambdas.dat", "w");
1974  fprintf(f, "#");
1975  cpl_table_dump(aGeo->table, 0, 100000000, f);
1976  fclose(f);
1977 #endif
1978  unsigned short nslice;
1979  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1980  /* get the entries for this one slice */
1981  cpl_table_select_all(gt->table);
1982  cpl_table_and_selected_int(gt->table, "SliceCCD", CPL_EQUAL_TO, nslice);
1983  if (cpl_table_count_selected(gt->table) < 1) {
1984  continue;
1985  }
1986  cpl_array *asel = cpl_table_where_selected(gt->table);
1987  cpl_table *slice = cpl_table_extract_selected(gt->table);
1988 #if 0
1989  cpl_table_dump(slice, 0, 1000, stdout);
1990  fflush(stdout);
1991 #endif
1992 
1993  /* compute weighted averages for the geometrical properties */
1994  double a, ae, w, we, xr, xre, dxl = 0., dxr = 0.;
1995  muse_geo_determine_horizontal_wmean(slice, MUSE_GEOTABLE_ANGLE,
1996  MUSE_GEOTABLE_ANGLE"err", &a, &ae,
1997  NULL, 2.);
1998  muse_geo_determine_horizontal_wmean(slice, MUSE_GEOTABLE_WIDTH,
1999  MUSE_GEOTABLE_WIDTH"err", &w, &we,
2000  NULL, 1.5);
2001  muse_geo_determine_horizontal_wmean(slice, "xrel", "xrelerr", &xr, &xre,
2002  NULL, 1.5);
2003  muse_geo_determine_horizontal_wmean(slice, "dxl", NULL, &dxl, NULL, NULL, 2.);
2004  muse_geo_determine_horizontal_wmean(slice, "dxr", NULL, &dxr, NULL, NULL, 2.);
2005 
2006  /* compute the average +/- stdev of the "vpos" values, too */
2007  cpl_errorstate ps = cpl_errorstate_get();
2008  double p = NAN, pe = -1, pm = NAN;
2009  muse_geo_determine_horizontal_vpos(slice, &p, &pe, &pm,
2010  pinholedy, fmaskrot);
2011  if (pe < 0 && !cpl_errorstate_is_equal(ps)) { /* some error, possibly only a single value */
2012  pe = 0.1; /* better something large than something negative... */
2013  cpl_errorstate_set(ps);
2014  }
2015 
2016  cpl_msg_debug(__func__, "IFU %2hhu stack %1d slice %2d / %2d "
2017  "angle %6.3f +/- %.3f deg width %.3f +/- %.3f pix "
2018  "xrel %.4f +/- %.4f vpos %.4f +/- %.4f (%.4f)", ifu,
2019  cpl_table_get_int(slice, "stack", 0, NULL), nslice,
2020  cpl_table_get_int(slice, "SliceSky", 0, NULL), a, ae, w, we,
2021  xr, xre, p, pe, pm);
2022  /* replace first selected line with the compute properties, *
2023  * unselect it then erase the other selected ones */
2024  cpl_size irow = cpl_array_get_cplsize(asel, 0, NULL);
2025  cpl_array_delete(asel);
2026  cpl_table_set_double(gt->table, MUSE_GEOTABLE_ANGLE, irow, a);
2027  cpl_table_set_double(gt->table, MUSE_GEOTABLE_ANGLE"err", irow, ae);
2028  cpl_table_set_double(gt->table, MUSE_GEOTABLE_WIDTH, irow, w);
2029  cpl_table_set_double(gt->table, MUSE_GEOTABLE_WIDTH"err", irow, we);
2030  cpl_table_set_double(gt->table, "xrel", irow, xr);
2031  cpl_table_set_double(gt->table, "xrelerr", irow, xre);
2032  cpl_table_set_double(gt->table, "vpos", irow, p);
2033  cpl_table_set_double(gt->table, "vposerr", irow, pe);
2034  cpl_table_set_double(gt->table, "dxl", irow, dxl);
2035  cpl_table_set_double(gt->table, "dxr", irow, dxr);
2036  /* invalidate some columns that do not make sense any more, do not erase *
2037  * them yet, so that other tables of the same basic format can be appended */
2038  cpl_table_set_invalid(gt->table, "flux", irow);
2039  cpl_table_set_invalid(gt->table, "lambda", irow);
2040  cpl_table_set_invalid(gt->table, "xc", irow);
2041  cpl_table_set_invalid(gt->table, "yc", irow);
2042  cpl_table_unselect_row(gt->table, irow);
2043  cpl_table_erase_selected(gt->table);
2044  cpl_table_delete(slice);
2045  } /* for nslice (all slices) */
2046 #if 0
2047  printf("intermediate result: weighted averages of angle, width, and xrel:\n");
2048  cpl_table_dump(gt->table, 0, 1000000, stdout);
2049  fflush(stdout);
2050 #endif
2051 
2052  /* Now go through one slicer stack, and for each slice with a result find *
2053  * the other three (horizontally adjacent) slices. Compute the gaps between *
2054  * the slices and add them to the total budget to derive the central *
2055  * horizontal position of all four slices in this row of the slicer stack. */
2056  /* XXX how does the angle (of the mask, each IFU, or *
2057  * the individual slices) affect the result? */
2058  const unsigned short nsoff = kMuseSlicesPerCCD / 4; /* slice offset */
2059  for (nslice = 1 + nsoff; nslice <= 2*nsoff; nslice++) {
2060  /* get the entries for this one slice by sky numbering */
2061  cpl_table_unselect_all(gt->table);
2062  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice - nsoff);
2063  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice);
2064  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice + nsoff);
2065  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice + 2*nsoff);
2066  /* extract the selected rows so that we can access them easily */
2067  cpl_table *ts = cpl_table_extract_selected(gt->table);
2068  /* get the row indices for all four slicer stacks (if possible) */
2069  int irow, nrowts = cpl_table_get_nrow(ts),
2070  i1 = -1, i2 = -1, i3 = -1, i4 = -1;
2071  for (irow = 0; irow < nrowts; irow++) {
2072  /* here, we index the stacks again in the same way as the *
2073  * optical numbering, from right to left in the field of view */
2074  switch (cpl_table_get_int(ts, "stack", irow, NULL)) {
2075  case 1: i1 = irow; break;
2076  case 2: i2 = irow; break;
2077  case 3: i3 = irow; break;
2078  case 4: i4 = irow; break;
2079  } /* switch */
2080  } /* for irow */
2081  /* at least the two central slicer stacks are required */
2082  if (i3 < 0 || i2 < 0) {
2083  char *msg = cpl_sprintf("For IFU %2hhu / row %2d in the slicer stacks "
2084  "(slice sky numbers %02d, %02d, %02d, %02d), at "
2085  "least one of the two middle stacks (%s/%s) is "
2086  "missing", ifu, nslice - nsoff,
2087  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff,
2088  i3 < 0 ? "left" : "-", i2 < 0 ? "right" : "-");
2089  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "%s", msg);
2090  cpl_msg_error(__func__, "%s", msg);
2091  cpl_free(msg);
2092  cpl_table_dump(ts, 0, 10000, stdout);
2093  cpl_table_delete(ts);
2094  continue;
2095  }
2096 
2097  double *xrel = cpl_table_get_data_double(ts, "xrel"),
2098  *xrerr = cpl_table_get_data_double(ts, "xrelerr"),
2099  *width = cpl_table_get_data_double(ts, MUSE_GEOTABLE_WIDTH),
2100  *werr = cpl_table_get_data_double(ts, MUSE_GEOTABLE_WIDTH"err"),
2101  *pdxl = cpl_table_get_data_double(ts, "dxl"),
2102  *pdxr = cpl_table_get_data_double(ts, "dxr");
2103  /* also need the slice width in mm here, so create a new temporary column */
2104  cpl_table_duplicate_column(ts, "width_mm",
2105  ts, MUSE_GEOTABLE_WIDTH);
2106  cpl_table_multiply_scalar(ts, "width_mm", 1. / kScaleX);
2107  cpl_table_set_column_unit(ts, "width_mm", "mm");
2108  cpl_table_duplicate_column(ts, "widtherr_mm",
2109  ts, MUSE_GEOTABLE_WIDTH"err");
2110  cpl_table_multiply_scalar(ts, "widtherr_mm", 1. / kScaleX);
2111  cpl_table_set_column_unit(ts, "widtherr_mm", "mm");
2112  double *wmm = cpl_table_get_data_double(ts, "width_mm"),
2113  *werrmm = cpl_table_get_data_double(ts, "widtherr_mm");
2114 
2115  double cgap1 = (3. * kMuseCUmpmDX / fmaskrot
2116  - (xrel[i3] + wmm[i3] / 2. + wmm[i2] / 2. - xrel[i2])) /* [mm] */
2117  * kScaleX,
2118  cgerr = sqrt(pow(xrerr[i3], 2) + pow(xrerr[i2], 2) +
2119  pow(werrmm[i3] / 2., 2) + pow(werrmm[i2] / 2., 2))
2120  * kScaleX,
2121  cgap2 = kMuseCUmpmDX * (1. - pdxl[i3] - pdxr[i2]) /* [mm] */
2122  * kScaleX,
2123  cgap = stdgap ? cgap1 : cgap2;
2124 #if 0
2125  cpl_msg_debug(__func__, "cgap: %f, %f +/- %f", cgap1, cgap2, cgerr);
2126 #endif
2127  /* for unlikely size, set error and output error message (the *
2128  * error state might otherwise be swallowed by parallelization!) */
2129  if (cgap < 0 || cgap > 0.5) {
2130  cpl_msg_debug(__func__, "For IFU %2hhu / row %2d in the slicer stacks "
2131  "(slice sky numbers %02d, %02d, %02d, %02d), the central "
2132  "gap is unlikely (%f), reset to %.2f pix", ifu,
2133  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2134  nslice + 2*nsoff, cgap, kMuseGeoMiddleGap);
2135  cgerr += sqrt(fabs(cgap)); /* add to the error estimate */
2136  cgap = kMuseGeoMiddleGap;
2137  }
2138  /* use this to compute effective centers of the two middle *
2139  * slices, distributing the gap equally to both sides */
2140  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i3, -(cgap / 2. + width[i3] / 2.));
2141  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i2, cgap / 2. + width[i2] / 2.);
2142  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i3,
2143  sqrt(cgerr*cgerr + werr[i3]*werr[i3]) / 2.);
2144  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i2,
2145  sqrt(cgerr*cgerr + werr[i2]*werr[i2]) / 2.);
2146 
2147  double lgap = NAN, lgerr = NAN;
2148  if (i4 >= 0) {
2149  /* now compute the left gap, using the central *
2150  * gap and the widths of the two left slices */
2151  lgerr = sqrt(pow(xrerr[i4], 2) + pow(xrerr[i3], 2) +
2152  pow(werrmm[i4] / 2., 2) + pow(werrmm[i3] / 2., 2))
2153  * kScaleX;
2154  double lgap1 = (3. * kMuseCUmpmDX / fmaskrot
2155  - (xrel[i4] + wmm[i4] / 2. + wmm[i3] / 2. - xrel[i3]))
2156  * kScaleX,
2157  lgap2 = kMuseCUmpmDX * (1. - pdxl[i4] - pdxr[i3]) /* [mm] */
2158  * kScaleX;
2159  lgap = stdgap ? lgap1 : lgap2;
2160 #if 0
2161  cpl_msg_debug(__func__, "lgap: %f, %f +/- %f", lgap1, lgap2, lgerr);
2162 #endif
2163  if (lgap < 0 || lgap > 0.5) {
2164  cpl_msg_debug(__func__, "For IFU %2hhu / row %2d in the slicer stacks"
2165  " (slice sky numbers %02d, %02d, %02d, %02d), the left "
2166  "gap is unlikely (%f), reset to %.2f pix", ifu,
2167  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2168  nslice + 2*nsoff, lgap, kMuseGeoOuterGap);
2169  lgerr += sqrt(fabs(lgap)); /* add to the error estimate */
2170  lgap = kMuseGeoOuterGap;
2171  }
2172  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i4,
2173  -(cgap / 2. + width[i3] + lgap + width[i4] / 2.));
2174  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i4,
2175  sqrt(cgerr*cgerr / 4. + werr[i3]*werr[i3]
2176  + lgerr*lgerr + werr[i4]*werr[i4] / 4.));
2177  } else {
2178  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "For IFU %2hhu /"
2179  " row %2d in the slicer stacks (slice sky numbers"
2180  " %02d, %02d, %02d, %02d), the leftmost stack is "
2181  "missing", ifu, nslice - nsoff,
2182  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff);
2183  }
2184 
2185  double rgap = NAN, rgerr = NAN;
2186  if (i1 >= 0) {
2187  /* and finally the right gap and the position of the rightmost slice */
2188  rgerr = sqrt(pow(xrerr[i2], 2) + pow(xrerr[i1], 2) +
2189  pow(werrmm[i2] / 2., 2) + pow(werrmm[i1] / 2., 2))
2190  * kScaleX;
2191  double rgap1 = (3. * kMuseCUmpmDX / fmaskrot
2192  - (xrel[i2] + wmm[i2] / 2. + wmm[i1] / 2. - xrel[i1]))
2193  * kScaleX,
2194  rgap2 = kMuseCUmpmDX * (1. - pdxl[i2] - pdxr[i1]) /* [mm] */
2195  * kScaleX;
2196  rgap = stdgap ? rgap1 : rgap2;
2197 #if 0
2198  cpl_msg_debug(__func__, "rgap: %f, %f +/- %f", rgap1, rgap2, rgerr);
2199 #endif
2200  if (rgap < 0 || rgap > 0.5) {
2201  cpl_msg_debug(__func__, "For IFU %2hhu / row %2d in the slicer stacks"
2202  " (slice sky numbers %02d, %02d, %02d, %02d), the right"
2203  " gap is unlikely (%f), reset to %.2f pix", ifu,
2204  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2205  nslice + 2*nsoff, rgap, kMuseGeoOuterGap);
2206  rgerr += sqrt(fabs(rgap)); /* add to the error estimate */
2207  rgap = kMuseGeoOuterGap;
2208  }
2209  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i1,
2210  cgap / 2. + width[i2] + rgap + width[i1] / 2.);
2211  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i1,
2212  sqrt(cgerr*cgerr / 4. + werr[i2]*werr[i2]
2213  + rgerr*rgerr + werr[i1]*werr[i1] / 4.));
2214  } else {
2215  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "For IFU %2hhu /"
2216  " row %2d in the slicer stacks (slice sky numbers"
2217  " %02d, %02d, %02d, %02d), the rightmost stack is"
2218  " missing", ifu, nslice - nsoff,
2219  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff);
2220  }
2221  cpl_msg_debug(__func__, "IFU %2hhu row %2d gaps (slice sky numbers %02d, "
2222  "%02d, %02d, %02d): central %.3f +/- %.3f pix, left %.3f +/- "
2223  "%.3f pix, right %.3f +/- %.3f pix", ifu, nslice - nsoff,
2224  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff,
2225  cgap, cgerr, lgap, lgerr, rgap, rgerr);
2226 
2227  /* remove the temporary column again... */
2228  cpl_table_erase_column(ts, "width_mm");
2229  cpl_table_erase_column(ts, "widtherr_mm");
2230 
2231  /* erase the original entries in the gt->table, and replace them *
2232  * with the ones we modified here, which contain the center positions */
2233  cpl_table_erase_selected(gt->table);
2234  cpl_table_insert(gt->table, ts, cpl_table_get_nrow(gt->table));
2235  cpl_table_delete(ts);
2236  } /* for nslice (all slices in the middle left stack) */
2237 
2238  /* sort gt->table again, by decreasing stack and increasing sky number */
2239  cpl_propertylist *order = cpl_propertylist_new();
2240  cpl_propertylist_append_bool(order, "stack", CPL_TRUE);
2241  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2242  cpl_table_sort(gt->table, order);
2243  cpl_propertylist_delete(order);
2244 #if 0
2245  printf("intermediate result: only the y position is still missing:\n");
2246  cpl_table_dump(gt->table, 0, 1000000, stdout);
2247  fflush(stdout);
2248 #endif
2249  return gt;
2250 } /* muse_geo_determine_horizontal() */
2251 
2252 /*----------------------------------------------------------------------------*/
2266 /*----------------------------------------------------------------------------*/
2267 static unsigned char
2268 muse_geo_select_reference(const muse_geo_table *aGeo, unsigned short *aSlice)
2269 {
2270  unsigned char ifu = 0;
2271  unsigned short slice = 0;
2272  /* copy table to not touch the original */
2273  cpl_table *geo = cpl_table_duplicate(aGeo->table);
2274  cpl_table_unselect_all(geo);
2275  cpl_table_or_selected_int(geo, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, 12);
2276  cpl_table_and_selected_int(geo, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2277  int nsel = cpl_table_count_selected(geo);
2278  if (nsel > 0) {
2279  ifu = 12;
2280  slice = 24;
2281  }
2282  unsigned char testifu = 13,
2283  testslice = 18;
2284  short testoffset = 1;
2285  while (ifu == 0 && slice == 0) {
2286  cpl_table_unselect_all(geo);
2287  cpl_table_or_selected_int(geo, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, testifu);
2288  cpl_table_and_selected_int(geo, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, testslice);
2289  nsel = cpl_table_count_selected(geo);
2290  if (nsel > 0) {
2291  ifu = testifu;
2292  slice = testslice;
2293  }
2294  testifu += testoffset;
2295  if (testifu > kMuseNumIFUs) { /* reached the end, start again from the middle */
2296  testifu = 11;
2297  testoffset = -1;
2298  }
2299  } /* while */
2300  cpl_table_delete(geo);
2301  if (aSlice) {
2302  *aSlice = slice;
2303  }
2304  return ifu;
2305 } /* muse_geo_select_reference() */
2306 
2307 /*----------------------------------------------------------------------------*/
2358 /*----------------------------------------------------------------------------*/
2359 cpl_error_code
2361 {
2362  cpl_ensure_code(aGeo && aGeo->table && aSpots, CPL_ERROR_NULL_INPUT);
2363  cpl_size nrow = cpl_table_get_nrow(aGeo->table);
2364  cpl_ensure_code(nrow >= 50, CPL_ERROR_ILLEGAL_INPUT);
2365  cpl_ensure_code(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
2366  CPL_ERROR_INCOMPATIBLE_INPUT);
2367  cpl_ensure_code(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
2368  CPL_ERROR_INCOMPATIBLE_INPUT);
2369  const unsigned char ifu1 = cpl_table_get_column_min(aGeo->table, MUSE_GEOTABLE_FIELD),
2370  ifu2 = cpl_table_get_column_max(aGeo->table, MUSE_GEOTABLE_FIELD);
2371  if (!ifu1 || !ifu2 || ifu1 == ifu2) {
2372  return cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
2373  "input geometry table contains data of IFUs "
2374  "%2hhu .. %2hhu", ifu1, ifu2);
2375  }
2376  cpl_ensure_code(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
2377  CPL_ERROR_ILLEGAL_INPUT);
2378  cpl_array *hoffsets = NULL;
2379  if (getenv("MUSE_GEOMETRY_HORI_OFFSETS")) {
2381  getenv("MUSE_GEOMETRY_HORI_OFFSETS"), ",");
2382  cpl_msg_warning(__func__, "Overriding horizontal offsets, found %"
2383  CPL_SIZE_FORMAT" values!", cpl_array_get_size(hoffsets));
2384  }
2385  const double kScaleX = kMuseTypicalCubeSizeX * aGeo->scale / 60.;
2386 
2387  /* create and fill the SliceSky and stack table columns in the spots table */
2388  cpl_table_new_column(aSpots, MUSE_GEOTABLE_SKY, CPL_TYPE_INT);
2389  cpl_table_new_column(aSpots, "stack", CPL_TYPE_INT);
2390  cpl_size ispot, nspots = cpl_table_get_nrow(aSpots);
2391  for (ispot = 0; ispot < nspots; ispot++) {
2392  int nslice = cpl_table_get_int(aSpots, "SliceCCD", ispot, NULL);
2393  cpl_table_set(aSpots, MUSE_GEOTABLE_SKY, ispot, kMuseGeoSliceSky[nslice - 1]);
2394  unsigned char stack = nslice <= 12 ? 4 : (nslice <= 24 ? 3 : (nslice <= 36 ? 2 : 1));
2395  cpl_table_set_int(aSpots, "stack", ispot, stack);
2396  } /* for ispot */
2397 
2398  /* extract top and bottom rows of the middle slicer stack *
2399  * to make the table smaller and better to handle */
2400  const unsigned short nsoff = kMuseSlicesPerCCD / 4; /* slice offset */
2401  cpl_table_unselect_all(aSpots);
2402  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 - nsoff + 1);
2403  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2404  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + 1);
2405  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + nsoff);
2406 #if 0
2407  cpl_msg_debug(__func__, "All top/bottom spots selected: %"
2408  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2409 #endif
2410  /* also apply a lower flux limit of 500. */
2411  cpl_table_and_selected_double(aSpots, "flux", CPL_NOT_LESS_THAN, 500.);
2412 #if 0
2413  cpl_msg_debug(__func__, "All bright top/bottom spots selected: %"
2414  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2415 #endif
2416  /* only select the middle spot in each slice */
2417  cpl_table_and_selected_int(aSpots, "SpotNo", CPL_EQUAL_TO, 2);
2418 #if 0
2419  cpl_msg_debug(__func__, "All bright and central top/bottom spots selected: %"
2420  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2421 #endif
2422  cpl_table *tbspots = cpl_table_extract_selected(aSpots); /* top/bottom spots */
2423  cpl_msg_debug(__func__, "All spots: %"CPL_SIZE_FORMAT", top/bottom spots to "
2424  "work with: %"CPL_SIZE_FORMAT, nspots, cpl_table_get_nrow(tbspots));
2425  nspots = cpl_table_get_nrow(tbspots);
2426 
2427  /* variables for the corrective loops */
2428  int *ifus = cpl_table_get_data_int(aGeo->table, MUSE_GEOTABLE_FIELD),
2429  *slices = cpl_table_get_data_int(aGeo->table, MUSE_GEOTABLE_SKY);
2430  double *xpos = cpl_table_get_data_double(aGeo->table, MUSE_GEOTABLE_X),
2431  *xerr = cpl_table_get_data_double(aGeo->table, MUSE_GEOTABLE_X"err"),
2432  *xrel = cpl_table_get_data_double(aGeo->table, "xrel");
2433  int irow;
2434 
2435  /* loop over the IFUs */
2436  unsigned char nifu;
2437  for (nifu = ifu1; nifu < ifu2; nifu++) { /* exclude the last IFU! */
2438  /* select and extract bottom slices of current IFU */
2439  cpl_table_unselect_all(tbspots);
2440  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2441  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + nsoff);
2442  cpl_table_and_selected_int(tbspots, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
2443  cpl_table *xspots = cpl_table_extract_selected(tbspots);
2444  /* select and extract top slices of next IFU (below) */
2445  cpl_table_unselect_all(tbspots);
2446  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 - nsoff + 1);
2447  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + 1);
2448  cpl_table_and_selected_int(tbspots, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu + 1);
2449  cpl_table *tmp = cpl_table_extract_selected(tbspots);
2450  cpl_table_insert(xspots, tmp, cpl_table_get_nrow(xspots));
2451  cpl_table_delete(tmp);
2452  int nxspots = cpl_table_get_nrow(xspots);
2453 
2454  /* sort the table, mainly to get ordered output for debugging */
2455  cpl_propertylist *order = cpl_propertylist_new();
2456  cpl_propertylist_append_bool(order, "lambda", CPL_FALSE);
2457  cpl_propertylist_append_bool(order, "VPOS", CPL_FALSE);
2458  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2459  cpl_table_sort(xspots, order);
2460  cpl_propertylist_delete(order);
2461 #if 0
2462  printf("IFU %2hhu:\n", nifu);
2463  cpl_table_dump(xspots, 0, nxspots, stdout);
2464  fflush(stdout);
2465 #endif
2466 
2467  /* create array for the dxcen-statistics and respective index */
2468  cpl_array *apos2 = cpl_array_new(nxspots, CPL_TYPE_DOUBLE),
2469  *apos3 = cpl_array_new(nxspots, CPL_TYPE_DOUBLE);
2470  int idx2 = 0, idx3 = 0;
2471 
2472  /* loop over all wavelengths, and positions to find common spots in adjacent IFUs */
2473  cpl_vector *vtmp = cpl_vector_wrap(nxspots,
2474  cpl_table_get_data_double(xspots, "lambda")),
2475  *lambdas = muse_cplvector_get_unique(vtmp);
2476  cpl_vector_unwrap(vtmp);
2477  vtmp = cpl_vector_wrap(nxspots, cpl_table_get_data_double(xspots, "VPOS"));
2478  cpl_vector *positions = muse_cplvector_get_unique(vtmp);
2479  cpl_vector_unwrap(vtmp);
2480  int ilambda, nlambda = cpl_vector_get_size(lambdas);
2481  for (ilambda = 0; ilambda < nlambda; ilambda++) {
2482  double lambda = cpl_vector_get(lambdas, ilambda);
2483 
2484  int ipos, npos = cpl_vector_get_size(positions);
2485  for (ipos = 0; ipos < npos; ipos++) {
2486  double vpos = cpl_vector_get(positions, ipos);
2487 
2488  int nstack; /* two middle stacks, from left to right in the FOV */
2489  for (nstack = 3; nstack >= 2; nstack--) {
2490  cpl_table_select_all(xspots);
2491  cpl_table_and_selected_double(xspots, "lambda", CPL_EQUAL_TO, lambda);
2492  cpl_table_and_selected_double(xspots, "VPOS", CPL_EQUAL_TO, vpos);
2493  cpl_table_and_selected_int(xspots, "stack", CPL_EQUAL_TO, nstack);
2494  int nsel = cpl_table_count_selected(xspots);
2495  if (nsel != 2) {
2496 #if 0
2497  printf("IFU %2hhu, lambda = %f, VPOS = %f, stack = %d: %d selected rows!\n",
2498  nifu, lambda, vpos, nstack, nsel);
2499 #endif
2500  continue;
2501  }
2502  /* extract and sort the table of common positions */
2503  cpl_table *common = cpl_table_extract_selected(xspots);
2504  order = cpl_propertylist_new();
2505  cpl_propertylist_append_bool(order, "SubField", CPL_FALSE);
2506  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2507  cpl_table_sort(common, order);
2508  cpl_propertylist_delete(order);
2509  int nselthis = cpl_table_and_selected_int(common, "SubField",
2510  CPL_EQUAL_TO, nifu);
2511  cpl_table_select_all(common);
2512  int nselnext = cpl_table_and_selected_int(common, "SubField",
2513  CPL_EQUAL_TO, nifu + 1);
2514  if (nselthis != 1 || nselnext != 1) {
2515 #if 0
2516  printf("\nIFU %2hhu, lambda = %f, VPOS = %f, stack = %d: "
2517  "%d rows of IFU %2hhu, %d rows of IFU %2d!\n",
2518  nifu, lambda, vpos, nstack, nselthis, nifu, nselnext,
2519  (int)nifu + 1);
2520  cpl_table_dump(common, 0, 100000, stdout);
2521 #endif
2522  cpl_table_delete(common);
2523  continue;
2524  }
2525  /* now, this IFU is in row 0, the next IFU is in row 1, *
2526  * so we can finally compute the difference between both *
2527  * dxcen values, and store it in the array */
2528  double xdiff1 = cpl_table_get(common, "dxcen", 0, NULL),
2529  xdiff2 = cpl_table_get(common, "dxcen", 1, NULL),
2530  twidth1 = cpl_table_get(common, "twidth", 0, NULL),
2531  twidth2 = cpl_table_get(common, "twidth", 1, NULL);
2532  cpl_table_unselect_all(aGeo->table);
2533  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO,
2534  cpl_table_get_int(common, "SubField", 0, NULL));
2535  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO,
2536  cpl_table_get_int(common, "SliceSky", 0, NULL));
2537  cpl_array *sel = cpl_table_where_selected(aGeo->table);
2538  double width1 = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_WIDTH,
2539  cpl_array_get(sel, 0, NULL), NULL);
2540  cpl_array_delete(sel);
2541  cpl_table_unselect_all(aGeo->table);
2542  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO,
2543  cpl_table_get_int(common, "SubField", 1, NULL));
2544  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO,
2545  cpl_table_get_int(common, "SliceSky", 1, NULL));
2546  sel = cpl_table_where_selected(aGeo->table);
2547  double width2 = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_WIDTH,
2548  cpl_array_get(sel, 0, NULL), NULL);
2549  cpl_array_delete(sel);
2550 #if 0
2551  printf("\nIFU %2hhu, lambda = %f, VPOS = %f, stack + %d, twidths: %f / %f, geowidths: %f / %f:\n",
2552  nifu, lambda, vpos, nstack, twidth1, twidth2, width1, width2);
2553  cpl_table_dump(common, 0, 100000, stdout);
2554  printf("==> xdiff = %f pix, %f pix (corrected)\n", xdiff1 - xdiff2,
2555  xdiff1 * width1 / twidth1 - xdiff2 * width2 / twidth2);
2556  fflush(stdout);
2557 #endif
2558  double xdiff = xdiff1 * width1 / twidth1 - xdiff2 * width2 / twidth2;
2559  if (nstack == 3) {
2560  cpl_array_set(apos3, idx3++, xdiff);
2561  }
2562  if (nstack == 2) {
2563  cpl_array_set(apos2, idx2++, xdiff);
2564  }
2565  cpl_table_delete(common);
2566  } /* for nstack */
2567  } /* for ipos */
2568  } /* for ilambda */
2571 #define HIST_BIN_WIDTH 0.1 /* 1/10 pix width of each histogram bin */
2572  cpl_bivector *hist2 = muse_cplarray_histogram(apos2, HIST_BIN_WIDTH, -5., 5.),
2573  *hist3 = muse_cplarray_histogram(apos3, HIST_BIN_WIDTH, -5., 5.);
2574  /* use a gap of 2 histogram entries below 0.5 to define a gap */
2575  muse_cplarray_erase_outliers(apos2, hist2, 2, 0.5);
2576  muse_cplarray_erase_outliers(apos3, hist3, 2, 0.5);
2577  cpl_bivector_delete(hist2);
2578  cpl_bivector_delete(hist3);
2579  cpl_array *apos = cpl_array_new(0, cpl_array_get_type(apos2));
2580  cpl_array_insert(apos, apos2, 0);
2581  cpl_array_insert(apos, apos3, cpl_array_get_size(apos));
2582 #if 0
2583  char *fn = cpl_sprintf("apos2_%02hhuto%02hhu.pos", nifu, nifu+1);
2584  FILE *fp = fopen(fn, "w");
2585  fprintf(fp, "# apos2 %2hhu to %2hhu:\n#", nifu, nifu+1);
2586  cpl_array_dump(apos2, 0, 1000000, fp);
2587  fclose(fp);
2588  cpl_free(fn);
2589  fn = cpl_sprintf("apos3_%02hhuto%02hhu.pos", nifu, nifu+1);
2590  fp = fopen(fn, "w");
2591  fprintf(fp, "# apos3 %2hhu to %2hhu:\n#", nifu, nifu+1);
2592  cpl_array_dump(apos3, 0, 1000000, fp);
2593  fclose(fp);
2594  cpl_free(fn);
2595  fn = cpl_sprintf("apos_%02hhuto%02hhu.pos", nifu, nifu+1);
2596  fp = fopen(fn, "w");
2597  fprintf(fp, "# apos %2hhu to %2hhu:\n#", nifu, nifu+1);
2598  cpl_array_dump(apos, 0, 1000000, fp);
2599  fclose(fp);
2600  cpl_free(fn);
2601 #endif
2602  /* compute both average positions, their sigmas, and use that to create *
2603  * a combined weighted average value with a corresponding error estimate */
2604  double mean2 = cpl_array_get_mean(apos2),
2605  stdev2 = cpl_array_get_stdev(apos2),
2606  var2 = stdev2 * stdev2,
2607  mean3 = cpl_array_get_mean(apos3),
2608  stdev3 = cpl_array_get_stdev(apos3),
2609  var3 = stdev3 * stdev3,
2610  mean = (mean2 + mean3) / 2.,
2611  wmean = (mean2 / var2 + mean3 / var3) / (1. / var2 + 1. / var3),
2612  wstdev = sqrt(1. / (1. / var2 + 1. / var3));
2613  cpl_array_delete(apos2);
2614  cpl_array_delete(apos3);
2615  cpl_msg_debug(__func__, "IFU %2hhu to IFU %2d: %6.3f +/- %5.3f pix "
2616  "[stack 3: %6.3f +/- %5.3f, stack2: %6.3f +/- %5.3f ==> %6.3f"
2617  " or %6.3f +/- %5.3f (%6.3f)]", nifu, (int)nifu + 1, wmean, wstdev,
2618  mean3, stdev3, mean2, stdev2, mean,
2619  cpl_array_get_mean(apos), cpl_array_get_stdev(apos),
2620  cpl_array_get_median(apos));
2621  cpl_array_delete(apos);
2622  cpl_vector_delete(lambdas);
2623  cpl_vector_delete(positions);
2624  cpl_table_delete(xspots);
2625 
2626  /* now we have the offsets (and the error estimates), apply the shifts */
2627  for (irow = 0; irow < nrow; irow++) {
2628  if (ifus[irow] >= nifu + 1) {
2629  xpos[irow] -= wmean;
2630  xerr[irow] = sqrt(xerr[irow]*xerr[irow] + wstdev*wstdev);
2631  xrel[irow] += wmean / kScaleX;
2632  } /* if */
2633  } /* for irow (all table rows) */
2634  } /* for nifu */
2635  cpl_table_delete(tbspots);
2636 
2637  /* continue to see if there are manual adjustments to add */
2638  for (nifu = ifu1; nifu < ifu2; nifu++) { /* exclude the last IFU! */
2639  if (hoffsets) {
2640  double xdiff = 0.;
2641  if (cpl_array_get_size(hoffsets) >= nifu) { /* at least this many elements */
2642  const char *sdiff = cpl_array_get_string(hoffsets, nifu - 1);
2643  if (sdiff) {
2644  xdiff = atof(sdiff);
2645  } /* if override valid */
2646  } /* if override present from the next IFU */
2647  cpl_msg_debug(__func__, "Subtracting extra %7.4f pix from IFU %d onwards",
2648  xdiff, (int)nifu + 1);
2649  for (irow = 0; irow < nrow; irow++) {
2650  if (ifus[irow] >= nifu + 1) {
2651  xpos[irow] -= xdiff;
2652  /* don't care about xrel in this case */
2653  } /* if */
2654  } /* for irow (all table rows) */
2655  continue; /* go to next IFU */
2656  } /* if hoffsets is there (override active) */
2657 #if 0
2658  printf("%s after correcting %d:\n", __func__, (int)nifu + 1);
2659  cpl_table_dump(aGeo->table, 0, 1000000, stdout);
2660  fflush(stdout);
2661 #endif
2662  } /* for nifu */
2663  cpl_array_delete(hoffsets);
2664 
2665  /* find the horizontal center of the reference slice and shift the whole *
2666  * table so that the center is between the ref. slice and its neighbor */
2667  double xref = 0.;
2668  unsigned short sliceref;
2669  const unsigned char ifuref = muse_geo_select_reference(aGeo, &sliceref);
2670  for (irow = 0; irow < nrow; irow++) {
2671  if (ifus[irow] == ifuref &&
2672  (slices[irow] == sliceref || slices[irow] == sliceref + 12)) {
2673  xref += xpos[irow];
2674  } /* if */
2675  } /* for irow (all table rows) */
2676  xref /= 2.; /* current mean center */
2677  cpl_msg_debug(__func__, "Reference point (IFU %2hhu, SliceSky %2hu/%2d) "
2678  "currently centered at %f pix, correcting this offset", ifuref,
2679  sliceref, (int)sliceref + 12, xref);
2680  cpl_table_subtract_scalar(aGeo->table, MUSE_GEOTABLE_X, xref);
2681 
2682  return CPL_ERROR_NONE;
2683 } /* muse_geo_refine_horizontal() */
2684 
2685 /*----------------------------------------------------------------------------*/
2735 /*----------------------------------------------------------------------------*/
2738 {
2739  cpl_ensure(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT, NULL);
2740  int nrow = cpl_table_get_nrow(aGeo->table);
2741  cpl_ensure(nrow >= 10, CPL_ERROR_ILLEGAL_INPUT, NULL);
2742  cpl_ensure(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
2743  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
2744  int spotmin = cpl_table_get_column_min(aGeo->table, "spot"),
2745  spotmax = cpl_table_get_column_max(aGeo->table, "spot");
2746  cpl_ensure(spotmin == spotmax, CPL_ERROR_ILLEGAL_INPUT, NULL);
2747  const double kScaleY = 60. / aGeo->scale / kMuseTypicalCubeSizeY;
2748 
2749  /* duplicate the input table, but already remove some useless lines */
2751  cpl_table_erase_column(gt->table, "dxerr");
2752  cpl_table_erase_column(gt->table, "dxl");
2753  cpl_table_erase_column(gt->table, "dxr");
2754  cpl_table_erase_column(gt->table, "xc");
2755  cpl_table_erase_column(gt->table, "yc");
2756  cpl_table_erase_column(gt->table, "dx");
2757  cpl_table_erase_column(gt->table, "flux");
2758  cpl_table_erase_column(gt->table, "lambda");
2759 
2760  /* set the vertical position value */
2761  // XXX this needs to take into account the angle and the relative shift of the middle spot
2762  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_Y, 0, nrow, 0.);
2763  cpl_table_add_columns(gt->table, MUSE_GEOTABLE_Y, "vpos");
2764  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y, "mm");
2765  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_Y"err", 0, nrow, 0.);
2766  cpl_table_add_columns(gt->table, MUSE_GEOTABLE_Y"err", "vposerr");
2767  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y"err", "mm");
2768 #if 0
2769  printf("\nfull geometry table, with absolute \"y\" [mm]:\n");
2770  cpl_table_dump(gt->table, 0, nrow, stdout);
2771  fflush(stdout);
2772 #endif
2773 
2774  double maskangle = 0., fmaskrot = 1.;
2775  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
2776  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
2777  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
2778  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
2779  "(cos = %.4e)", maskangle, fmaskrot);
2780  }
2781  double pinholedy = kMuseCUmpmDY;
2782  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
2783  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
2784  cpl_msg_info(__func__, "Using pinhole y distance of %f mm (instead of "
2785  "%f mm)", pinholedy, kMuseCUmpmDY);
2786  }
2787  cpl_boolean printgoing = getenv("MUSE_DEBUG_GEO_VERTICAL")
2788  && atoi(getenv("MUSE_DEBUG_GEO_VERTICAL")) > 0;
2789 
2790  unsigned short middleSlice;
2791  const unsigned char middleIFU = muse_geo_select_reference(aGeo, &middleSlice);
2792  cpl_msg_info(__func__, "Using IFU %2hhu / SliceSky %2hu as reference",
2793  middleIFU, middleSlice);
2794  double ycentral = NAN,
2795  ymax = -DBL_MAX, ymin = DBL_MAX;
2796  int irow;
2797  for (irow = 0; irow < nrow; irow++) {
2798  unsigned char ifu = cpl_table_get_int(gt->table, MUSE_GEOTABLE_FIELD, irow, NULL);
2799  unsigned short slice = cpl_table_get_int(gt->table, MUSE_GEOTABLE_SKY, irow, NULL);
2800  double y = cpl_table_get_double(gt->table, MUSE_GEOTABLE_Y, irow, NULL);
2801  if (ifu == middleIFU && slice == middleSlice) {
2802  ycentral = y;
2803  break;
2804  }
2805  if (y > ymax) {
2806  ymax = y;
2807  }
2808  if (y < ymin) {
2809  ymin = y;
2810  }
2811  } /* for irow (all table rows) */
2812  if (!isfinite(ycentral)) { /* average min and max */
2813  ycentral = (ymin + ymax) / 2.;
2814  cpl_msg_info(__func__, "Averaged the y range to ycentral = %f pix", ycentral);
2815  } else {
2816  cpl_msg_info(__func__, "Found IFU %2hhu, slice %2hu, using its y value as "
2817  "ycentral = %f pix", middleIFU, middleSlice, ycentral);
2818  }
2819  cpl_table_subtract_scalar(gt->table, MUSE_GEOTABLE_Y, ycentral);
2820  const unsigned short nsoff = kMuseSlicesPerCCD / 4, /* slice offset */
2821  nstack[] = { 3, 2, 4, 1, 0 };
2822  unsigned char i;
2823  for (i = 0; nstack[i] > 0; i++) {
2824  /* unselect middle-left slicer stack and delete the rest */
2825  cpl_table_unselect_all(gt->table);
2826  cpl_table_or_selected_int(gt->table, "stack", CPL_EQUAL_TO, nstack[i]);
2827  cpl_table *tstack = cpl_table_extract_selected(gt->table);
2828  int ntsrow = cpl_table_get_nrow(tstack);
2829  /* sort the stack table by subfield and then slice number on sky */
2830  cpl_propertylist *sorting = cpl_propertylist_new();
2831  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
2832  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
2833  cpl_table_sort(tstack, sorting);
2834  cpl_propertylist_delete(sorting);
2835  /* find the slice in the same slicer row as the reference slice from above */
2836  const unsigned short refslice = middleSlice - (nstack[i] - 3) * nsoff;
2837  cpl_table_select_all(tstack);
2838  cpl_table_and_selected_int(tstack, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, middleIFU);
2839  cpl_table_and_selected_int(tstack, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, refslice);
2840  if (cpl_table_count_selected(tstack) <= 0) {
2841  char *msg = cpl_sprintf("reference slice %2hu of reference IFU %2hhu not "
2842  "found in slicer stack %hu!", refslice,
2843  middleIFU, nstack[i]);
2844  cpl_msg_error(__func__, "%s", msg);
2845  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "%s", msg);
2846  cpl_free(msg);
2847  cpl_table_delete(tstack);
2848  continue;
2849  }
2850  cpl_table_erase_selected(gt->table);
2851  cpl_array *asel = cpl_table_where_selected(tstack);
2852  int iref = cpl_array_get(asel, 0, NULL);
2853  cpl_array_delete(asel);
2854  double yref = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, iref, NULL);
2855  cpl_msg_info(__func__, "reference slice %2hu of reference IFU %2hhu found at row"
2856  " %d in slicer stack %hu!", refslice, middleIFU, iref, nstack[i]);
2857  /* Maybe the reference pinhole in this slicer stack was illuminated by a *
2858  * different pinhole than the one in the reference stack. Then we need to *
2859  * subtract one pinhole distance from the y-positions in this stack. */
2860  if (fabs(yref) > pinholedy / 2.) {
2861  cpl_msg_info(__func__, "%s vertical pinhole distance (%f) to "
2862  "recenter stack %hu", yref < 0. ? "adding" : "subtracting",
2863  pinholedy, nstack[i]);
2864  cpl_table_add_scalar(tstack, MUSE_GEOTABLE_Y, yref < 0. ? pinholedy : -pinholedy);
2865  yref = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, iref, NULL);
2866  }
2867  /* go back in the table from the reference slice, the input y values *
2868  * should decrease, the output y value are supposed to increase */
2869  cpl_table_new_column(tstack, "dy", CPL_TYPE_DOUBLE);
2870  cpl_table_set_column_unit(tstack, "dy", "mm");
2871  cpl_table_set_column_format(tstack, "dy", "%9.5f");
2872  cpl_table_set_double(tstack, "dy", iref, yref);
2873  /* keep the current y-position in a temporary column, "ycopy" */
2874  cpl_table_duplicate_column(tstack, "ycopy", tstack, MUSE_GEOTABLE_Y);
2875  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, iref, yref);
2876  double yprev = cpl_table_get_double(tstack, "ycopy", iref, NULL),
2877  dyoffset = pinholedy * fmaskrot,
2878  ysum = yref;
2879  int iprev;
2880  for (irow = iref - 1, iprev = iref; irow >= 0; iprev = irow, irow--) {
2881  /* see how many slices apart we are from the previous */
2882  int difu = cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, iprev, NULL)
2883  - cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, irow, NULL),
2884  dslice = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, iprev, NULL)
2885  - cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, irow, NULL);
2886  if (difu != 0) {
2887  dslice += nsoff;
2888  }
2889  double y = cpl_table_get_double(tstack, "ycopy", irow, NULL),
2890  dy = y - yprev, /* should be negative here, around -120 um */
2891  dexpect = dslice * kScaleY,
2892  dratio = dy / dexpect,
2893  dycor = 0.; /* correction to y distance, zero by default */
2894  if (dratio < -8.) { /* negative ratio here */
2895  dycor = 2 * dyoffset;
2896  } else if (dratio < -2.) {
2897  dycor = dyoffset;
2898  } else if (dratio > 5.) {
2899  dycor = -dyoffset;
2900  }
2901  if (printgoing) {
2902  cpl_msg_debug(__func__, "going back: %d %d, %f %f --> diff %9.6f "
2903  "expected %f ratio %9.6f --> dy = %f", difu, dslice,
2904  yprev, y, dy, dexpect, dratio, dy + dycor);
2905  }
2906  dy += dycor;
2907  if (abs(difu) > 1) { /* make a gap, of somewhat arbitrary size */
2908  double gap = abs(difu) * kMuseGeoIFUVGap;
2909  dy -= gap;
2910  cpl_msg_warning(__func__, "%d missing IFUs, guessing distance as %f",
2911  abs(difu) - 1, gap);
2912  }
2913  cpl_table_set_double(tstack, "dy", irow, dy);
2914  ysum += dy;
2915  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, irow, ysum);
2916  yprev = y;
2917  } /* for irow (lower half of the stack table) */
2918  /* go forward in the table from the reference slice, the input y values *
2919  * should increase, the output y value are supposed to decrease */
2920  yprev = cpl_table_get_double(tstack, "ycopy", iref, NULL);
2921  ysum = yref; /* start summing with the first y step from the ref. slice */
2922  for (irow = iref + 1, iprev = iref; irow < ntsrow; iprev = irow, irow++) {
2923  int difu = cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, iprev, NULL)
2924  - cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, irow, NULL),
2925  dslice = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, iprev, NULL)
2926  - cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, irow, NULL);
2927  if (difu != 0) {
2928  dslice -= nsoff;
2929  }
2930  double y = cpl_table_get_double(tstack, "ycopy", irow, NULL),
2931  dy = y - yprev, /* should be positive here, around +120 um */
2932  dexpect = -dslice * kScaleY,
2933  dratio = dy / dexpect,
2934  dycor = 0.; /* correction to y distance, zero by default */
2935  if (dratio > 8.) { /* positive ratio here */
2936  dycor = -2 * dyoffset;
2937  } else if (dratio > 2.) {
2938  dycor = -dyoffset;
2939  } else if (dratio < -5.) {
2940  dycor = dyoffset;
2941  }
2942  if (printgoing) {
2943  cpl_msg_debug(__func__, "going forward: %d %d, %f %f --> diff %9.6f "
2944  "expected %f ratio %9.6f --> dy = %f", difu, dslice,
2945  yprev, y, dy, dexpect, dratio, dy + dycor);
2946  }
2947  dy += dycor;
2948  if (abs(difu) > 1) { /* make a gap, of somewhat arbitrary size */
2949  double gap = abs(difu) * kMuseGeoIFUVGap;
2950  dy += gap;
2951  cpl_msg_warning(__func__, "%d missing IFUs, guessing distance as %f",
2952  abs(difu) - 1, gap);
2953  }
2954  cpl_table_set_double(tstack, "dy", irow, dy);
2955  ysum += dy;
2956  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, irow, ysum);
2957  yprev = y;
2958  } /* for irow (upper half of the stack table) */
2959  if (printgoing) {
2960  printf("\ngeometry table of slicer stack %hu, with \"dy\" [mm]:\n",
2961  nstack[i]);
2962  cpl_table_dump(tstack, 0, nrow, stdout);
2963  fflush(stdout);
2964  }
2965  /* done with the temporary columns "ycopy" and "dy" */
2966  cpl_table_erase_column(tstack, "ycopy");
2967  cpl_table_erase_column(tstack, "dy");
2968  /* insert this slicer stack data back into the main table */
2969  cpl_table_insert(gt->table, tstack, cpl_table_get_nrow(gt->table));
2970  cpl_table_delete(tstack);
2971  } /* for i (all slicer stacks in a particular order) */
2972 
2973  /* The above should have correctly derived the vertical central positions *
2974  * of the slices as seens from the data on the CCD. This does not work *
2975  * correctly for the slices at the top and/or bottom of each IFU, since *
2976  * they may lie within the shadow of the adjacent IFU. *
2977  * To correct this, go though all stacks in all IFUs separately, and try *
2978  * to reset the vertical distance between the two outermost slices to the *
2979  * average distance. */
2980  cpl_msg_info(__func__, "Correcting vertical position of top/bottom slices");
2981  unsigned char ifu;
2982  for (ifu = 1; ifu <= kMuseNumIFUs; ifu++) {
2983  for (i = 0; nstack[i] > 0; i++) {
2984  cpl_table_unselect_all(gt->table);
2985  cpl_table_or_selected_int(gt->table, "SubField", CPL_EQUAL_TO, ifu);
2986  cpl_table_and_selected_int(gt->table, "stack", CPL_EQUAL_TO, nstack[i]);
2987  if (!cpl_table_count_selected(gt->table)) {
2988  continue; /* next stack or IFU */
2989  }
2990  cpl_table *tstack = cpl_table_extract_selected(gt->table);
2991  int ntsrow = cpl_table_get_nrow(tstack);
2992  /* sort the stack table by sky slice number */
2993  cpl_propertylist *sorting = cpl_propertylist_new();
2994  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
2995  cpl_table_sort(tstack, sorting);
2996  cpl_propertylist_delete(sorting);
2997 
2998  /* first and last entry should now be the edge slices, but verify this */
2999  unsigned short nslicetop = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, 0, NULL),
3000  nslicebot = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY,
3001  ntsrow - 1, NULL);
3002  cpl_boolean hastop = nslicetop % nsoff == 1,
3003  hasbot = nslicebot % nsoff == 0,
3004  haschanged = CPL_FALSE; /* if we changed the value(s) */
3005 #if 0
3006  printf("table of IFU %2hhu / SliceSky %2hu: %hu to %hu (%s, %s)\n",
3007  ifu, nstack[i], nslicetop, nslicebot,
3008  hastop ? "has top" : "does NOT have top",
3009  hasbot ? "has bottom" : "does NOT have bottom");
3010  cpl_table_dump(tstack, 0, 1000, stdout);
3011  fflush(stdout);
3012 #endif
3013 
3014  cpl_vector *vvpos = cpl_vector_new(ntsrow - 3),
3015  *vverr = cpl_vector_new(ntsrow - 3);
3016  for (irow = 1; irow < ntsrow - 2; irow++) {
3017  double dy = fabs(cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, irow + 1, NULL)
3018  - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, irow, NULL)),
3019  dye1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", irow, NULL),
3020  dye2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", irow + 1, NULL),
3021  dyerr = sqrt(dye1*dye1 + dye2*dye2);
3022  cpl_vector_set(vvpos, irow - 1, dy);
3023  cpl_vector_set(vverr, irow - 1, dyerr);
3024  } /* for irow (slices 2 .. 11 in this stack) */
3025  /* XXX should use a weighted mean but for now use the *
3026  * standard means of value and error vectors */
3027  double mean = cpl_vector_get_mean(vvpos),
3028  median = cpl_vector_get_median_const(vvpos),
3029  stdev = cpl_vector_get_stdev(vvpos),
3030  stdev2 = cpl_vector_get_mean(vverr);
3031  cpl_vector_delete(vvpos);
3032  cpl_vector_delete(vverr);
3033  cpl_msg_debug(__func__, "dy of IFU %2hhu / SliceSky %2hu: %f +/- %f (%f) [+/- %f]",
3034  ifu, nstack[i], mean, stdev, median, stdev2);
3035  if (hastop) {
3036  /* y-distance at top */
3037  double ytop = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, 0, NULL),
3038  dyt = fabs(ytop - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, 1, NULL)),
3039  dyt1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", 0, NULL),
3040  dyt2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", 1, NULL),
3041  dyterr = sqrt(dyt1*dyt1 + dyt2*dyt2);
3042  cpl_msg_debug(__func__, "dy of IFU %2hhu / SliceSky %2hu (top): %f +/- %f",
3043  ifu, nstack[i], dyt, dyterr);
3044  /* if the difference in distance is larger than the combined *
3045  * 1-sigma errors, then apply the difference as correction */
3046  if (mean - dyt > sqrt(dyterr*dyterr + stdev2*stdev2)) {
3047  /* add the difference to the y-position of the first (=top) slice */
3048  ytop += mean - dyt;
3049  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, 0, ytop);
3050  haschanged = CPL_TRUE;
3051 #if 0
3052  printf("new top entry (+%f):\n", mean - dyt);
3053  cpl_table_dump(tstack, 0, 1, stdout);
3054  fflush(stdout);
3055 #endif
3056  } /* if diff greater than error */
3057  } /* if hastop */
3058  if (hasbot) {
3059  /* y-distance (and error) at bottom */
3060  double ybot = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 1, NULL),
3061  dyb = fabs(ybot - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 2, NULL)),
3062  dyb1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", ntsrow - 2, NULL),
3063  dyb2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", ntsrow - 1, NULL),
3064  dyberr = sqrt(dyb1*dyb1 + dyb2*dyb2);
3065  cpl_msg_debug(__func__, "dy of IFU %2hhu / SliceSky %2hu (bottom): %f +/- %f",
3066  ifu, nstack[i], dyb, dyberr);
3067  if (mean - dyb > sqrt(dyberr*dyberr + stdev2*stdev2)) {
3068  /* subtract the difference from the y-position of the last (=bottom) slice */
3069  ybot -= mean - dyb;
3070  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 1, ybot);
3071  haschanged = CPL_TRUE;
3072 #if 0
3073  printf("new bottom entry (-%f):\n", mean - dyb);
3074  cpl_table_dump(tstack, ntsrow - 1, 1, stdout);
3075  fflush(stdout);
3076 #endif
3077  } /* if diff greater than error */
3078  } /* if hasbot */
3079 
3080  /* if we changed something, append the changed *
3081  * table portion back to the main gt->table */
3082  if (haschanged) {
3083  cpl_table_erase_selected(gt->table);
3084  cpl_table_insert(gt->table, tstack, cpl_table_get_nrow(gt->table));
3085  }
3086  cpl_table_delete(tstack);
3087  } /* for i (slicer stacks) */
3088  } /* for ifu */
3089 
3090  /* convert "y" column from [mm] to [pix] */
3091  cpl_table_divide_scalar(gt->table, MUSE_GEOTABLE_Y, kMuseSpaxelSizeY_WFM / aGeo->scale);
3092  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y, "pix");
3093  cpl_table_divide_scalar(gt->table, MUSE_GEOTABLE_Y"err", kMuseSpaxelSizeY_WFM / aGeo->scale);
3094  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y"err", "pix");
3095 
3096  /* now we can delete the remaining useless lines in the output table */
3097  cpl_table_erase_column(gt->table, "spot");
3098  cpl_table_erase_column(gt->table, "xrel");
3099  cpl_table_erase_column(gt->table, "xrelerr");
3100  cpl_table_erase_column(gt->table, "vpos");
3101  cpl_table_erase_column(gt->table, "vposerr");
3102  cpl_table_erase_column(gt->table, "stack");
3103  return gt;
3104 } /* muse_geo_determine_vertical() */
3105 
3106 /*----------------------------------------------------------------------------*/
3140 /*----------------------------------------------------------------------------*/
3141 cpl_error_code
3143 {
3144  cpl_ensure_code(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT);
3145  cpl_ensure_code(cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_FIELD) &&
3146  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_CCD) &&
3147  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_SKY) &&
3148  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_X) &&
3149  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_Y) &&
3150  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_ANGLE) &&
3151  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_WIDTH),
3152  CPL_ERROR_ILLEGAL_INPUT);
3153 
3154  /* correct the computed values by the wrong pinhole vertical distance */
3155  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
3156 #if 0
3157  FILE *fn1 = fopen("gt1.ascii", "w");
3158  fprintf(fn1, "geometry table before scaling\n");
3159  cpl_table_dump(aGeo->table, 0, 10000, fn1);
3160  fclose(fn1);
3161 #endif
3162  double pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY")),
3163  fdy = kMuseCUmpmDY / pinholedy;
3164  cpl_msg_info(__func__, "Pinhole y distance of %f mm was used instead of "
3165  "%f mm; scaling coordinates by %f!", pinholedy, kMuseCUmpmDY,
3166  fdy);
3167  int irow, nrow = cpl_table_get_nrow(aGeo->table);
3168  for (irow = 0; irow < nrow; irow++) {
3169  int err;
3170  /* scale the y coordinate directly */
3171  double y = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_Y, irow, &err);
3172  if (!err) {
3173  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_Y, irow, y * fdy);
3174  }
3175  /* Compute a vertical scale for the old angle, and scale that *
3176  * to compute the new angle, so that I don't have to use the *
3177  * approximate small-scale linearity of the tan() function. */
3178  double angleold = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, &err);
3179  if (!err) {
3180  double anglenew = atan(fdy * tan(angleold * CPL_MATH_RAD_DEG))
3181  * CPL_MATH_DEG_RAD;
3182  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, anglenew);
3183  }
3184  } /* for irow (all table rows) */
3185 #if 0
3186  FILE *fn2 = fopen("gt2.ascii", "w");
3187  fprintf(fn2, "geometry table after scaling by %f\n", fdy);
3188  cpl_table_dump(aGeo->table, 0, 10000, fn2);
3189  fclose(fn2);
3190 #endif
3191  } /* if */
3192 
3193  /* pad table with nonsense data for missing slices, but ignore missing IFUs */
3194  unsigned char nifu;
3195  for (nifu = 1; nifu <= kMuseNumIFUs; nifu++) {
3196  cpl_table_select_all(aGeo->table);
3197  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3198  if (cpl_table_count_selected(aGeo->table) < 1) {
3199  continue; /* this IFU is missing completely, skip it */
3200  }
3201 
3202  unsigned short nslice;
3203  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
3204  cpl_table_select_all(aGeo->table);
3205  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3206  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_CCD, CPL_EQUAL_TO, nslice);
3207  if (cpl_table_count_selected(aGeo->table) > 0) {
3208  continue; /* this slice is there */
3209  }
3210  /* add this missing slice */
3211  cpl_table_set_size(aGeo->table, cpl_table_get_nrow(aGeo->table) + 1);
3212  int irow = cpl_table_get_nrow(aGeo->table) - 1;
3213  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_FIELD, irow, nifu);
3214  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_CCD, irow, nslice);
3215  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_SKY, irow,
3216  kMuseGeoSliceSky[nslice - 1]);
3217  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_X, irow, NAN);
3218  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_Y, irow, NAN);
3219  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, 0.);
3220  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_WIDTH, irow, 0.);
3221  } /* for nslice */
3222  } /* for nifu */
3223 
3224  cpl_boolean needsnoflip = getenv("MUSE_GEOMETRY_NO_INVERSION")
3225  && atoi(getenv("MUSE_GEOMETRY_NO_INVERSION")) > 0;
3226  if (!needsnoflip) { /* just flip all slices */
3227  cpl_msg_info(__func__, "Flipping all slices because of data inversion!");
3228  cpl_table_multiply_scalar(aGeo->table, MUSE_GEOTABLE_Y, -1.);
3229  cpl_table_multiply_scalar(aGeo->table, MUSE_GEOTABLE_ANGLE, -1.);
3230  }
3231 
3232  /* sort the full table, by subfield and then slice number on sky */
3233  cpl_propertylist *sorting = cpl_propertylist_new();
3234  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
3235  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
3236  cpl_table_sort(aGeo->table, sorting);
3237  cpl_propertylist_delete(sorting);
3238 
3239 #if 0
3240  printf("\nfinal geometry table with all slicer stacks [pix]:\n");
3241  cpl_table_dump(aGeo->table, 0, nrow, stdout);
3242  fflush(stdout);
3243 #endif
3244 #if 0
3245  const char *fn = "GEOMETRY_TABLE.ascii";
3246  FILE *fp = fopen(fn, "w");
3247  fprintf(fp, "# final geometry table with all slicer stacks [pix]:\n");
3248  cpl_table_dump(aGeo->table, 0, nrow, fp);
3249  fclose(fp);
3250  cpl_msg_debug(__func__, "written geometry table in ASCII to \"%s\"", fn);
3251 #endif
3252 
3253  return CPL_ERROR_NONE;
3254 } /* muse_geo_finalize() */
3255 
3256 /*----------------------------------------------------------------------------*/
3271 /*----------------------------------------------------------------------------*/
3272 static unsigned int
3273 muse_geo_correct_slices_stack(cpl_table *aTStack, cpl_matrix *aPos,
3274  const char *aCol, const char *aErr, double aLimit,
3275  double aSigma)
3276 {
3277  const char *id = "muse_geo_correct_slices"; /* pretend to be in that function */
3278  /* pointers to relevant table column contents */
3279  double *pval = cpl_table_get_data_double(aTStack, aCol),
3280  *perr = cpl_table_get_data_double(aTStack, aErr);
3281  /* create vectors out of them for fitting */
3282  int ntsrow = cpl_table_get_nrow(aTStack);
3283  cpl_vector *val = cpl_vector_wrap(ntsrow, pval),
3284  *err = cpl_vector_wrap(ntsrow, perr);
3285  /* fit, without iteration, so use DBL_MAX for the sigma rejection level */
3286  double rms, chisq;
3287  cpl_polynomial *poly = muse_utils_iterate_fit_polynomial(aPos, val, err, NULL,
3288  1, DBL_MAX, &rms,
3289  &chisq);
3290  rms = sqrt(rms);
3291  cpl_vector_unwrap(val);
3292  cpl_vector_unwrap(err);
3293 #if 0
3294  cpl_msg_debug(__func__, "poly for %s values (RMS = %f, ChiSq = %f):", aCol,
3295  rms, chisq);
3296  cpl_polynomial_dump(poly, stdout);
3297  fflush(stdout);
3298 #endif
3299  unsigned int nreplaced = 0;
3300  int irow;
3301  for (irow = 0; irow < ntsrow; irow++) {
3302  double pos = cpl_matrix_get(aPos, 0, irow),
3303  vpoly = cpl_polynomial_eval_1d(poly, pos, NULL),
3304  dpoly = fabs(pval[irow] - vpoly);
3305  if (perr[irow] > aLimit || dpoly > aSigma * rms) {
3306  cpl_msg_debug(__func__, "Changing %s(%02.0f) from %.3f to %.3f "
3307  "(perr = %.3f > %.3f or dpoly = %.3f > %.3f)", aCol, pos,
3308  pval[irow], vpoly, perr[irow], aLimit, dpoly, aSigma * rms);
3309  pval[irow] = vpoly;
3310  nreplaced++;
3311  }
3312  } /* for irow */
3313  cpl_polynomial_delete(poly);
3314  if (nreplaced > 3) {
3315  cpl_msg_warning(id, "Changed %d of %d %s values in IFU %02d (stack with "
3316  "sky slices %d to %d)", nreplaced, ntsrow, aCol,
3317  cpl_table_get_int(aTStack, MUSE_GEOTABLE_FIELD, 0, NULL),
3318  (int)cpl_matrix_get(aPos, 0, 0),
3319  (int)cpl_matrix_get(aPos, 0, ntsrow - 1));
3320  }
3321  return nreplaced;
3322 } /* muse_geo_correct_slices_stack() */
3323 
3324 /*----------------------------------------------------------------------------*/
3362 /*----------------------------------------------------------------------------*/
3363 cpl_error_code
3364 muse_geo_correct_slices(muse_geo_table *aGeo, cpl_propertylist *aHeader,
3365  double aSigma)
3366 {
3367  cpl_ensure_code(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT);
3368  cpl_ensure_code(aSigma > 0., CPL_ERROR_ILLEGAL_INPUT);
3369  cpl_ensure_code(cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_FIELD) &&
3370  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_CCD) &&
3371  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_SKY) &&
3372  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_X) &&
3373  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_Y) &&
3374  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_ANGLE) &&
3375  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_WIDTH) &&
3376  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_X"err") &&
3377  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_Y"err") &&
3378  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_ANGLE"err") &&
3379  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_WIDTH"err"),
3380  CPL_ERROR_DATA_NOT_FOUND);
3381  cpl_ensure_code(cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_X) == CPL_TYPE_DOUBLE &&
3382  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_Y) == CPL_TYPE_DOUBLE &&
3383  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_ANGLE) == CPL_TYPE_DOUBLE &&
3384  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_WIDTH) == CPL_TYPE_DOUBLE &&
3385  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_X"err") == CPL_TYPE_DOUBLE &&
3386  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_Y"err") == CPL_TYPE_DOUBLE &&
3387  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_ANGLE"err") == CPL_TYPE_DOUBLE &&
3388  cpl_table_get_column_type(aGeo->table, MUSE_GEOTABLE_WIDTH"err") == CPL_TYPE_DOUBLE,
3389  CPL_ERROR_INCOMPATIBLE_INPUT);
3390 
3391  /* set output formats for readable screen dumps */
3392  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_X, "%8.3f");
3393  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_X"err", "%8.3f");
3394  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_Y, "%8.3f");
3395  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_Y"err", "%8.3f");
3396  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_ANGLE, "%5.3f");
3397  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_ANGLE"err", "%5.3f");
3398  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_WIDTH, "%8.3f");
3399  cpl_table_set_column_format(aGeo->table, MUSE_GEOTABLE_WIDTH"err", "%8.3f");
3400 
3401  cpl_msg_info(__func__, "Correcting %s using %.2f-sigma level",
3402  MUSE_TAG_GEOMETRY_TABLE, aSigma);
3403  cpl_msg_debug(__func__, " median errors: x %.3f y %.3f angle %.3f width %.3f",
3404  cpl_table_get_column_median(aGeo->table, MUSE_GEOTABLE_X"err"),
3405  cpl_table_get_column_median(aGeo->table, MUSE_GEOTABLE_Y"err"),
3406  cpl_table_get_column_median(aGeo->table, MUSE_GEOTABLE_ANGLE"err"),
3407  cpl_table_get_column_median(aGeo->table, MUSE_GEOTABLE_WIDTH"err"));
3408  /* set up limits for the allowed error estimates */
3409  const double kXLimit = 0.90, /* [pix], far too large, but otherwise it changes half the slices! */
3410  kYLimit = 0.10, /* [pix] */
3411  kALimit = 0.07, /* [pix] */
3412  kWLimit = 0.25; /* [pix] */
3413  cpl_msg_debug(__func__, " table limits: x %.3f y %.3f angle %.3f width %.3f",
3414  kXLimit, kYLimit, kALimit, kWLimit);
3415 
3416  int nx = 0, ny = 0, na = 0, nw = 0; /* counters to track changes */
3417  const unsigned short nsoff = kMuseSlicesPerCCD / 4; /* slice offset */
3418  unsigned char nifu;
3419  for (nifu = 1; nifu <= kMuseNumIFUs; nifu++) {
3420  unsigned char nstack;
3421  for (nstack = 1; nstack <= 4; nstack++) {
3422  /* derive slice sky numbers for this stack */
3423  unsigned short nslice1 = (nstack - 1) * nsoff + 1,
3424  nslice2 = nstack * nsoff;
3425  /* select whole slicer stack */
3426  cpl_table_unselect_all(aGeo->table);
3427  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3428  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_SKY,
3429  CPL_NOT_LESS_THAN, nslice1);
3430  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_SKY,
3431  CPL_NOT_GREATER_THAN, nslice2);
3432  int ntsrow = cpl_table_count_selected(aGeo->table);
3433  cpl_msg_debug(__func__, "IFU %2hhu stack %hhu, slices %2hu to %2hu: %d rows",
3434  nifu, nstack, nslice1, nslice2, ntsrow);
3435  if (ntsrow < 1) {
3436  continue; /* nothing to correct */
3437  }
3438 
3439  cpl_table *tstack = cpl_table_extract_selected(aGeo->table);
3440 #if 0
3441  cpl_table_dump(tstack, 0, nsoff, stdout);
3442  fflush(stdout);
3443 #endif
3444  /* sort the stack table by subfield and then slice number on sky *
3445  * (just to be really sure!) */
3446  cpl_propertylist *sorting = cpl_propertylist_new();
3447  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
3448  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
3449  cpl_table_sort(tstack, sorting);
3450  cpl_propertylist_delete(sorting);
3451 
3452  /* create a 1-row matrix of positions using the sky slice number */
3453  cpl_table_cast_column(tstack, MUSE_GEOTABLE_SKY, "skydouble", CPL_TYPE_DOUBLE);
3454  cpl_matrix *pos = cpl_matrix_wrap(1, ntsrow,
3455  cpl_table_get_data_double(tstack, "skydouble"));
3456  /* now do the linear fits and replacements of deviant entries */
3457  nx += muse_geo_correct_slices_stack(tstack, pos, MUSE_GEOTABLE_X,
3458  MUSE_GEOTABLE_X"err", kXLimit, aSigma);
3459  ny += muse_geo_correct_slices_stack(tstack, pos, MUSE_GEOTABLE_Y,
3460  MUSE_GEOTABLE_Y"err", kYLimit, aSigma);
3461  na += muse_geo_correct_slices_stack(tstack, pos, MUSE_GEOTABLE_ANGLE,
3462  MUSE_GEOTABLE_ANGLE"err", kALimit, aSigma);
3463  nw += muse_geo_correct_slices_stack(tstack, pos, MUSE_GEOTABLE_WIDTH,
3464  MUSE_GEOTABLE_WIDTH"err", kWLimit, aSigma);
3465  cpl_matrix_unwrap(pos);
3466  cpl_table_erase_column(tstack, "skydouble");
3467 #if 0
3468  cpl_msg_debug(__func__, "IFU %2hhu stack %hhu, final table:", nifu, nstack);
3469  cpl_table_dump(tstack, 0, nsoff, stdout);
3470  fflush(stdout);
3471 #endif
3472  cpl_table_erase_selected(aGeo->table);
3473  cpl_table_insert(aGeo->table, tstack, cpl_table_get_nrow(aGeo->table));
3474  cpl_table_delete(tstack);
3475  } /* for nstack */
3476  } /* for nifu */
3477 
3478  cpl_msg_info(__func__, "Changed %d x values, %d y values, %d angles, and %d "
3479  "widths.", nx, ny, na, nw);
3480  if (aHeader) {
3481  cpl_propertylist_update_int(aHeader, QC_GEO_SMOOTH_NX, nx);
3482  cpl_propertylist_update_int(aHeader, QC_GEO_SMOOTH_NY, ny);
3483  cpl_propertylist_update_int(aHeader, QC_GEO_SMOOTH_NA, na);
3484  cpl_propertylist_update_int(aHeader, QC_GEO_SMOOTH_NW, nw);
3485  }
3486  return CPL_ERROR_NONE;
3487 } /* muse_geo_correct_slices() */
3488 
3489 /*---------------------------------------------------------------------------*/
3506 /*---------------------------------------------------------------------------*/
3507 cpl_error_code
3508 muse_geo_qc_global(const muse_geo_table *aGeoTable, cpl_propertylist *aHeader)
3509 {
3510  cpl_ensure_code(aGeoTable && aHeader, CPL_ERROR_NULL_INPUT);
3511  cpl_table *geotable = aGeoTable->table; /* shortcut */
3512 
3513  /* compute the positions of the central gaps */
3514  cpl_array *agaps = cpl_array_new(kMuseNumIFUs, CPL_TYPE_DOUBLE);
3515  unsigned char nifu,
3516  nifu1 = cpl_table_get_column_min(geotable, MUSE_GEOTABLE_FIELD),
3517  nifu2 = cpl_table_get_column_max(geotable, MUSE_GEOTABLE_FIELD);
3518  for (nifu = nifu1; nifu <= nifu2; nifu++) {
3519  /* select 3rd stack (2nd from left) */
3520  cpl_table_unselect_all(geotable);
3521  cpl_table_or_selected_int(geotable, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3522  cpl_table_and_selected_int(geotable, MUSE_GEOTABLE_SKY, CPL_NOT_LESS_THAN, 13);
3523  cpl_table_and_selected_int(geotable, MUSE_GEOTABLE_SKY, CPL_NOT_GREATER_THAN, 24);
3524  cpl_table *tleft = cpl_table_extract_selected(geotable);
3525  /* select 2nd stack (3rd from left) */
3526  cpl_table_unselect_all(geotable);
3527  cpl_table_or_selected_int(geotable, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3528  cpl_table_and_selected_int(geotable, MUSE_GEOTABLE_SKY, CPL_NOT_LESS_THAN, 25);
3529  cpl_table_and_selected_int(geotable, MUSE_GEOTABLE_SKY, CPL_NOT_GREATER_THAN, 36);
3530  cpl_table *tright = cpl_table_extract_selected(geotable);
3531 
3532  /* check for the same table size */
3533  int irow, nrow = cpl_table_get_nrow(tleft),
3534  nrow2 = cpl_table_get_nrow(tright);
3535  if (nrow < 1 || nrow2 < 1) {
3536  cpl_msg_warning(__func__, "No slices for central stacks found, cannot "
3537  "compute gaps for QC in IFU %hhu", nifu);
3538  cpl_table_delete(tleft);
3539  cpl_table_delete(tright);
3540  continue;
3541  }
3542  if (nrow != nrow2) {
3543  cpl_msg_warning(__func__, "Unequal slice count for central stacks, cannot"
3544  " compute gaps for QC in IFU %hhu", nifu);
3545  cpl_table_delete(tleft);
3546  cpl_table_delete(tright);
3547  continue;
3548  }
3549  /* both tables should be sorted, but just to be sure not to compare *
3550  * slices with different vertical positions, sort them again */
3551  cpl_propertylist *sorting = cpl_propertylist_new();
3552  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
3553  cpl_table_sort(tleft, sorting);
3554  cpl_table_sort(tright, sorting);
3555  cpl_propertylist_delete(sorting);
3556  cpl_array *agap = cpl_array_new(nrow, CPL_TYPE_DOUBLE);
3557  for (irow = 0; irow < nrow; irow++) {
3558  double x1 = cpl_table_get(tleft, MUSE_GEOTABLE_X, irow, NULL),
3559  w1 = cpl_table_get(tleft, MUSE_GEOTABLE_WIDTH, irow, NULL),
3560  x2 = cpl_table_get(tright, MUSE_GEOTABLE_X, irow, NULL),
3561  w2 = cpl_table_get(tright, MUSE_GEOTABLE_WIDTH, irow, NULL);
3562  cpl_array_set_double(agap, irow, (x1 + w1/2. + x2 - w2/2.) / 2.);
3563  } /* for irow (all rows in both slicer stacks) */
3564  cpl_table_delete(tleft);
3565  cpl_table_delete(tright);
3566 
3567  /* now save the mean per-slicer-row gap in the per-IFU gaps array */
3568  double mean = cpl_array_get_mean(agap);
3569  cpl_array_set_double(agaps, nifu - 1, mean);
3570  char *kw = cpl_sprintf(QC_GEO_IFUi_GAP, nifu);
3571  cpl_propertylist_update_float(aHeader, kw, mean);
3572  cpl_free(kw);
3573  cpl_array_delete(agap);
3574  } /* for nifu (all IFUs) */
3575  double gmean = cpl_array_get_mean(agaps),
3576  gstdev = cpl_array_get_stdev(agaps);
3577  cpl_propertylist_update_float(aHeader, QC_GEO_GAPS_MEAN, gmean);
3578  cpl_propertylist_update_float(aHeader, QC_GEO_GAPS_STDEV, gstdev);
3579  cpl_array_delete(agaps);
3580 
3581  /* compute the global mask angle */
3582  double angle = cpl_table_get_column_mean(geotable, MUSE_GEOTABLE_ANGLE),
3583  astdev = cpl_table_get_column_stdev(geotable, MUSE_GEOTABLE_ANGLE),
3584  amedian = cpl_table_get_column_median(geotable, MUSE_GEOTABLE_ANGLE);
3585  cpl_errorstate state = cpl_errorstate_get();
3586  cpl_propertylist_update_float(aHeader, QC_GEO_MASK_ANGLE, amedian);
3587  if (!cpl_errorstate_is_equal(state)) {
3588  /* sometimes, this keyword gets detected as double type */
3589  cpl_errorstate_set(state);
3590  cpl_propertylist_update_double(aHeader, QC_GEO_MASK_ANGLE, amedian);
3591  }
3592 
3593  cpl_msg_info(__func__, "Added global QC keywords: angle = %.3f +/- %.3f "
3594  "(%.3f) deg, gap positions = %.3f +/- %.3f pix", angle, astdev,
3595  amedian, gmean, gstdev);
3596  return CPL_ERROR_NONE;
3597 } /* muse_geometry_qc_global() */
3598 
cpl_error_code muse_cplarray_erase_invalid(cpl_array *aArray)
Erase all invalid values from an array.
cpl_polynomial ** muse_trace_table_get_polys_for_slice(const cpl_table *aTable, const unsigned short aSlice)
construct polynomial from the trace table entry for the given slice
muse_geo_table * muse_geo_table_duplicate(const muse_geo_table *aGeo)
Make a copy of a MUSE geometry table.
Definition: muse_geo.c:1217
Structure definition for a collection of muse_images.
cpl_error_code muse_geo_refine_horizontal(muse_geo_table *aGeo, cpl_table *aSpots)
Refine relative horizontal positions of adjacent IFUs.
Definition: muse_geo.c:2360
const muse_cpltable_def muse_geo_measurements_def[]
Spots measurement table definition for geometrical calibration.
Definition: muse_geo.c:90
cpl_polynomial * muse_wave_table_get_poly_for_slice(const cpl_table *aTable, const unsigned short aSlice)
Construct polynomial from the wavelength calibration table entry for the given slice.
unsigned char muse_utils_get_ifu(const cpl_propertylist *aHeaders)
Find out the IFU/channel from which this header originated.
Definition: muse_utils.c:98
cpl_image * data
the data extension
Definition: muse_image.h:46
cpl_error_code muse_geo_correct_slices(muse_geo_table *aGeo, cpl_propertylist *aHeader, double aSigma)
Correct deviant slices in an existing MUSE geometry table.
Definition: muse_geo.c:3364
static void muse_geo_determine_horizontal_vpos(const cpl_table *aSlice, double *aP, double *aPE, double *aPM, double aDY, double aF)
Compute properly weighted vertical slice position mean from intermediate geometry table columns...
Definition: muse_geo.c:1784
static cpl_size muse_geo_determine_horizontal_wmean(const cpl_table *aSlice, const char *aCol, const char *aColErr, double *aValue, double *aError, double *aMedian, double aSigma)
Interatively reject outliers and compute (weighted) statistics of intermediate geometry table columns...
Definition: muse_geo.c:1630
const muse_cpltable_def muse_geo_table_def[]
Geometry table definition.
Definition: muse_geo.c:169
double muse_geo_table_ifu_area(const cpl_table *aTable, const unsigned char aIFU, double aScale)
Compute the area of an IFU in the VLT focal plane.
Definition: muse_geo.c:297
double muse_geo_compute_pinhole_global_distance(cpl_array *aDY, double aWidth, double aMin, double aMax)
Use vertical pinhole distance measurements of all IFUs to compute the effective global value...
Definition: muse_geo.c:1112
double scale
The VLT focal plane scale factor of the data. output file.
Definition: muse_geo.h:83
muse_geo_table * muse_geo_determine_vertical(const muse_geo_table *aGeo)
Use all properties of the central spot and the horizontal properties in each slice to compute the ver...
Definition: muse_geo.c:2737
cpl_vector * muse_cplvector_get_unique(const cpl_vector *aVector)
Separate out all unique entries in a given vector into a new one.
Structure definition of MUSE three extension FITS file.
Definition: muse_image.h:40
double muse_pfits_get_focu_scale(const cpl_propertylist *aHeaders)
find out the scale in the VLT focal plane
Definition: muse_pfits.c:951
static cpl_table * muse_geo_get_spot_peaks(cpl_table *aSpots, unsigned char aIFU, unsigned short aNSlice, unsigned char aNSpot, double aLambda, double aVPosRef, cpl_boolean aVerifyDY, cpl_array *aDY)
Use spot measurements of one IFU to compute vertical pinhole distance.
Definition: muse_geo.c:798
cpl_propertylist * header
the FITS header
Definition: muse_image.h:72
cpl_error_code muse_cpltable_check(const cpl_table *aTable, const muse_cpltable_def *aDef)
Check whether the table contains the fields of the definition.
unsigned int muse_imagelist_get_size(muse_imagelist *aList)
Return the number of stored images.
void muse_trace_polys_delete(cpl_polynomial *aPolys[])
Delete the multi-polynomial array created in relation to tracing.
muse_geo_table * muse_geo_determine_horizontal(const muse_geo_table *aGeo)
Use per-spot and per-wavelength partial geometry to determine the horizontal geometrical properties f...
Definition: muse_geo.c:1929
cpl_error_code muse_geo_compute_pinhole_local_distance(cpl_array *aDY, cpl_table *aSpots)
Use spot measurements of one IFU to compute vertical pinhole distance.
Definition: muse_geo.c:1016
cpl_table * muse_cpltable_new(const muse_cpltable_def *aDef, cpl_size aLength)
Create an empty table according to the specified definition.
cpl_table * muse_geo_measure_spots(muse_image *aImage, muse_imagelist *aList, const cpl_table *aTrace, const cpl_table *aWave, const cpl_vector *aLines, double aSigma, muse_geo_centroid_type aCentroid)
Detect spots on a combined image and measure them on the corresponding series of images.
Definition: muse_geo.c:458
cpl_array * muse_cplarray_new_from_delimited_string(const char *aString, const char *aDelim)
Convert a delimited string into an array of strings.
cpl_error_code muse_geo_qc_global(const muse_geo_table *aGeoTable, cpl_propertylist *aHeader)
Add the global QC parameters to the geometry table.
Definition: muse_geo.c:3508
muse_image * muse_imagelist_get(muse_imagelist *aList, unsigned int aIdx)
Get the muse_image of given list index.
cpl_table * table
The geometry table.
Definition: muse_geo.h:77
Structure definition of MUSE geometry table.
Definition: muse_geo.h:71
cpl_error_code muse_geo_finalize(muse_geo_table *aGeo)
Create a final version of a geometry table.
Definition: muse_geo.c:3142
muse_geo_centroid_type
Type of centroiding algorithm to use.
Definition: muse_geo.h:91
cpl_polynomial * muse_utils_iterate_fit_polynomial(cpl_matrix *aPos, cpl_vector *aVal, cpl_vector *aErr, cpl_table *aExtra, const unsigned int aOrder, const double aRSigma, double *aMSE, double *aChiSq)
Iterate a polynomial fit.
Definition: muse_utils.c:2234
cpl_bivector * muse_cplarray_histogram(const cpl_array *aArray, double aWidth, double aMin, double aMax)
Create a histogram for a numerical array.
int muse_pfits_get_posenc(const cpl_propertylist *aHeaders, unsigned short aEncoder)
query the absolute encoder position of one encoder
Definition: muse_pfits.c:1592
void muse_geo_table_delete(muse_geo_table *aGeo)
Deallocate memory associated to a geometry table object.
Definition: muse_geo.c:1237
muse_geo_table * muse_geo_table_new(cpl_size aNRows, double aScale)
Create a new MUSE geometry table.
Definition: muse_geo.c:1196
double muse_astro_posangle(const cpl_propertylist *aHeader)
Derive the position angle of an observation from information in a FITS header.
Definition: muse_astro.c:413
cpl_size muse_cplarray_erase_outliers(cpl_array *aArray, const cpl_bivector *aHistogram, cpl_size aGap, double aLimit)
Erase outliers from an array using histogram information.
double muse_pfits_get_pospos(const cpl_propertylist *aHeaders, unsigned short aEncoder)
query the position in user units of one encoder
Definition: muse_pfits.c:1621
cpl_table * muse_geo_table_extract_ifu(const cpl_table *aTable, const unsigned char aIFU)
Extract the part of a geometry table dealing with a given IFU.
Definition: muse_geo.c:235
Definition of a cpl table structure.
double muse_cplvector_get_adev_const(const cpl_vector *aVector, double aCenter)
Compute the average absolute deviation of a (constant) vector.
cpl_matrix * muse_matrix_new_gaussian_2d(int aXHalfwidth, int aYHalfwidth, double aSigma)
Create a matrix that contains a normalized 2D Gaussian.
Definition: muse_utils.c:1099
cpl_vector * muse_geo_lines_get(const cpl_table *aLines)
Select lines suitable for geometrical calibration from a line list.
Definition: muse_geo.c:354
muse_geo_table * muse_geo_determine_initial(cpl_table *aSpots, const cpl_table *aTrace)
Use spot measurements to compute initial geometrical properties.
Definition: muse_geo.c:1313