MUSE Pipeline Reference Manual  1.0.2
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-2014 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__, "line %d at %.3f Angstrom", iline + 1, lambda);
482 
483  unsigned short nslice;
484  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
485  cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
486  nslice),
487  *pwave = muse_wave_table_get_poly_for_slice(aWave, nslice);
488  if (!ptrace || !pwave) {
489  muse_trace_polys_delete(ptrace);
490  cpl_polynomial_delete(pwave);
491  continue;
492  }
493  double xc = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ny / 2, NULL);
494  /* extract a 1D polynomial at the approximate slice center, *
495  * since evaluating that is faster than the one in 2D */
496  cpl_polynomial *pxconst = cpl_polynomial_new(1);
497  cpl_size p = 0;
498  cpl_polynomial_set_coeff(pxconst, &p, xc);
499  cpl_polynomial *pywave = cpl_polynomial_extract(pwave, 0, pxconst);
500  cpl_polynomial_delete(pxconst);
501  /* search for y-position where the wavelength approximately matches */
502  double yc = 1, lbda = -1;
503  while (fabs(lambda - lbda) > 1.) {
504  lbda = cpl_polynomial_eval_1d(pywave, yc, NULL);
505  yc += 0.5; /* better do small steps */
506  if (yc > kMuseOutputYTop) { /* safeguard against infinite loop */
507  break;
508  }
509  } /* while */
510  cpl_polynomial_delete(pywave);
511 #if 0
512  cpl_msg_debug(__func__, "--> %.3f --> %f,%f in slice %hu",
513  lbda, xc, yc, nslice);
514 #endif
515  cpl_polynomial_delete(pwave);
516  /* if the difference is still large, then the polynomial *
517  * was faulty, so warn and continue with the next slice */
518  if (fabs(lambda - lbda) > 1.) {
519  cpl_msg_warning(__func__, "Polynomial in slice %hu of IFU %hhu appears "
520  "to be faulty! Skipping measurement of line %d (%.1f "
521  "Angstrom)", nslice, ifu, iline + 1, lambda);
522  continue;
523  }
524 
525  /* we are ~0.5 pix to high, but that should be fine on average, *
526  * because we now extract a box that should be large enough */
527 #define DETECTION_HALFSIZE 7
528  int xl = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], yc, NULL)),
529  xr = lround(cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], yc, NULL)),
530  yb = lround(yc - DETECTION_HALFSIZE),
531  yt = lround(yc + DETECTION_HALFSIZE);
532  cpl_image *box = cpl_image_extract(aImage->data, xl, yb, xr, yt);
533  /* smooth the image with a Gauss-filter before creating detection mask */
534  cpl_image *fbox = cpl_image_duplicate(box);
535  cpl_matrix *gkernel = muse_matrix_new_gaussian_2d(2, 2, 1.);
536  cpl_image_filter(fbox, box, gkernel, CPL_FILTER_LINEAR, CPL_BORDER_FILTER);
537  cpl_matrix_delete(gkernel);
538  cpl_stats_mode mode = CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV;
539  cpl_stats *s = cpl_stats_new_from_image(box, mode);
540  double limit = cpl_stats_get_median(s)
541  + aSigma * cpl_stats_get_median_dev(s);
542  cpl_mask *mask = cpl_mask_threshold_image_create(fbox, limit, DBL_MAX);
543 #if 0 /* file debug output for spot detection */
544  char *fn1 = cpl_sprintf("box_%02hu.fits", nslice),
545  *fn2 = cpl_sprintf("boxf_%02hu.fits", nslice),
546  *fn3 = cpl_sprintf("boxf_%02hu_mask.fits", nslice);
547  cpl_image_save(box, fn1, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
548  cpl_image_save(fbox, fn2, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
549  cpl_mask_save(mask, fn3, NULL, CPL_IO_CREATE);
550  cpl_free(fn1);
551  cpl_free(fn2);
552  cpl_free(fn3);
553 #endif
554  /* extract the apertures on the original unsmoothed image */
555  cpl_apertures *apertures = cpl_apertures_extract_mask(box, mask);
556 #if 0 /* debugging of box, statistics, and apertures */
557  cpl_msg_debug(__func__, "stats in box [%d:%d,%d:%d] --> limit = "
558  "%f + %.1f * %f = %f, apertures:", xl, xr, yb, yt,
559  cpl_stats_get_median(s), aSigma,
560  cpl_stats_get_median_dev(s), limit);
561  cpl_apertures_dump(apertures, stdout);
562  fflush(stdout);
563 #endif
564  cpl_mask_delete(mask);
565  cpl_stats_delete(s);
566  cpl_image_delete(fbox);
567  cpl_image_delete(box);
568  cpl_errorstate es = cpl_errorstate_get();
569  int nspots = cpl_apertures_get_size(apertures);
570  if (nspots < 0) {
571  nspots = 0;
572  }
573  if (!apertures || nspots != kMuseCUmpmSpotsPerSlice) {
574  cpl_msg_warning(__func__, "found %d spot%s (need %hhu) down to the %.1f"
575  "-sigma limit in slice %d for wavelength %.3f Angstrom "
576  "in box [%d:%d,%d:%d]", nspots, nspots == 1 ? "" : "s",
577  kMuseCUmpmSpotsPerSlice, aSigma, nslice, lambda,
578  xl, xr, yb, yt);
579  cpl_apertures_delete(apertures);
580  cpl_errorstate_set(es);
581  continue;
582  }
583  cpl_msg_debug(__func__, "found %d spots using the %.1f-sigma limit in "
584  "slice %d for wavelength %.3f Angstrom in box [%d:%d,%d:%d]",
585  nspots, aSigma, nslice, lambda, xl, xr, yb, yt);
586 #if 0
587  cpl_apertures_dump(apertures, stdout);
588  fflush(stdout);
589 #endif
590  /* Make sure that the spots are ordered in increasing x-pixel position. *
591  * Since the apertures cannot be sorted by that, use a temporary matrix *
592  * to sort and an index vector to store the sorted aperture numbers. */
593  cpl_matrix *mspots = cpl_matrix_new(nspots, 2);
594  int naper;
595  for (naper = 1; naper <= nspots; naper++) {
596  double xpos = cpl_apertures_get_centroid_x(apertures, naper);
597  cpl_matrix_set(mspots, naper - 1, 0, xpos);
598  cpl_matrix_set(mspots, naper - 1, 1, naper);
599  } /* for naper (all spot apertures) */
600  cpl_matrix_sort_rows(mspots, 1); /* this sorts by decreasing x-position! */
601 #if 0
602  printf("mspots sorted:\n");
603  cpl_matrix_dump(mspots, stdout);
604  fflush(stdout);
605 #endif
606  /* now create index buffer, with reverse order */
607  int idx, *aperidx = cpl_calloc(nspots, sizeof(int));
608  for (naper = 1, idx = nspots - 1; naper <= nspots && idx >= 0; naper++, idx--) {
609  /* save aperture number at correct index position */
610  aperidx[naper - 1] = cpl_matrix_get(mspots, idx, 1);
611  } /* for naper / i */
612  cpl_matrix_delete(mspots);
613 
614  /* now measure the three spots in each of the images of the list *
615  * and record the spot properties as well as those of the image */
616  unsigned int k;
617  for (k = 0; k < nimages; k++) {
618  muse_image *image = muse_imagelist_get(aList, k);
619  int posenc[4];
620  double pospos[4],
621  posang = muse_astro_posangle(image->header);
622  unsigned short i;
623  for (i = 1; i <= 4; i++) {
624  posenc[i-1] = muse_pfits_get_posenc(image->header, i);
625  pospos[i-1] = muse_pfits_get_pospos(image->header, i);
626  }
627 
628 #define MEASUREMENT_HALFSIZE 5
629 #define BACKGROUND_HALFSIZE 7
630  for (idx = 0; idx < nspots; idx++) {
631  naper = aperidx[idx];
632  double xpos = cpl_apertures_get_centroid_x(apertures, naper) + xl,
633  ypos = cpl_apertures_get_centroid_y(apertures, naper) + yb;
634 #if 0
635  printf("aper %d idx %d xpos %f\n", naper, idx, xpos);
636  fflush(stdout);
637 #endif
638  cpl_stats_mode mspot = CPL_STATS_FLUX | CPL_STATS_CENTROID,
639  mbg = CPL_STATS_MEAN;
640  cpl_stats *sspot = cpl_stats_new_from_image_window(image->data, mspot,
641  xpos - MEASUREMENT_HALFSIZE,
642  ypos - MEASUREMENT_HALFSIZE,
643  xpos + MEASUREMENT_HALFSIZE,
644  ypos + MEASUREMENT_HALFSIZE),
645  *sbg = cpl_stats_new_from_image_window(image->data, mbg,
646  xpos - BACKGROUND_HALFSIZE,
647  ypos - BACKGROUND_HALFSIZE,
648  xpos + BACKGROUND_HALFSIZE,
649  ypos + BACKGROUND_HALFSIZE);
650  int npix = cpl_stats_get_npix(sspot);
651  double bg = cpl_stats_get_mean(sbg),
652  flux = cpl_stats_get_flux(sspot) - bg * npix;
653  if (flux < 0.) {
654  flux = 0.;
655  }
656  cpl_stats_delete(sbg);
657  double xcentroid = cpl_stats_get_centroid_x(sspot),
658  ycentroid = cpl_stats_get_centroid_y(sspot);
659  cpl_stats_delete(sspot);
660  double xfwhm, yfwhm;
661  if (aCentroid == MUSE_GEO_CENTROID_GAUSSIAN) {
662  /* try a Gaussian fit to derive a better centroid position */
663  cpl_array *gpars = cpl_array_new(7, CPL_TYPE_DOUBLE);
664  cpl_array_set(gpars, 0, bg);
665  cpl_array_set(gpars, 1, flux);
666  cpl_array_set(gpars, 3, xcentroid);
667  cpl_array_set(gpars, 4, ycentroid);
668  cpl_array_set(gpars, 5, 2.); /* assume just critical sampling */
669  cpl_array_set(gpars, 6, 2.);
670  cpl_fit_image_gaussian(image->data, NULL, lround(xcentroid), lround(ycentroid),
671  2 * MEASUREMENT_HALFSIZE + 1, 2 * MEASUREMENT_HALFSIZE + 1,
672  gpars, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
673  xcentroid = cpl_array_get(gpars, 3, NULL);
674  ycentroid = cpl_array_get(gpars, 4, NULL);
675  xfwhm = cpl_array_get(gpars, 5, NULL) * CPL_MATH_FWHM_SIG;
676  yfwhm = cpl_array_get(gpars, 6, NULL) * CPL_MATH_FWHM_SIG;
677  cpl_array_delete(gpars);
678  } else {
679  cpl_image_get_fwhm(image->data, lround(xcentroid), lround(ycentroid),
680  &xfwhm, &yfwhm);
681  }
682  if (cpl_propertylist_has(image->header, MUSE_HDR_TMP_FN)) {
683  cpl_table_set_string(measurements, "filename", irow,
684  cpl_propertylist_get_string(image->header,
685  MUSE_HDR_TMP_FN));
686  } else {
687  cpl_table_set_string(measurements, "filename", irow, "unknown");
688  }
689  cpl_table_set_int(measurements, "image", irow, k + 1);
690  cpl_table_set_int(measurements, "POSENC2", irow, posenc[1]);
691  cpl_table_set(measurements, "POSPOS2", irow, pospos[1]);
692  cpl_table_set_int(measurements, "POSENC3", irow, posenc[2]);
693  cpl_table_set(measurements, "POSPOS3", irow, pospos[2]);
694  cpl_table_set_int(measurements, "POSENC4", irow, posenc[3]);
695  cpl_table_set(measurements, "POSPOS4", irow, pospos[3]);
696  /* compute the "real" vertical position of the mask, *
697  * as VPOS = POS3.POS / sin(POSANG) */
698  cpl_table_set(measurements, "VPOS", irow,
699  pospos[2] / sin(posang * CPL_MATH_RAD_DEG)); // XXX this is total rubbish, it will crash if POSANG==0!
700  cpl_table_set_double(measurements, "ScaleFOV", irow,
702  cpl_table_set_int(measurements, "SubField", irow, ifu);
703  cpl_table_set_int(measurements, "SliceCCD", irow, nslice);
704  cpl_table_set(measurements, "lambda", irow, lambda);
705  cpl_table_set_int(measurements, "SpotNo", irow, idx + 1);
706  cpl_table_set(measurements, "xc", irow, xcentroid);
707  cpl_table_set(measurements, "yc", irow, ycentroid);
708  cpl_table_set(measurements, "xfwhm", irow, xfwhm);
709  cpl_table_set(measurements, "yfwhm", irow, yfwhm);
710  cpl_table_set(measurements, "flux", irow, flux);
711  cpl_table_set(measurements, "bg", irow, bg);
712  /* also compute the relative position towards the slice center [pix] */
713  double xcslice = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ycentroid, NULL);
714  cpl_table_set(measurements, "dxcen", irow, xcentroid - xcslice);
715  /* and the slice width at this CCD position as traced [pix] */
716  double twidth = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], ycentroid, NULL)
717  - cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], ycentroid, NULL);
718  cpl_table_set(measurements, "twidth", irow, twidth);
719 #if 0 /* exclusion of cosmic rays or other rubbish by detection of weird *
720  * FWHM values did not work at all but excluded lots of valid points */
721  if (xfwhm < 0 || yfwhm < 0) {
722  cpl_msg_warning(__func__, "xfwhm and/or yfwhm are invalid: %f, %f:", xfwhm, yfwhm);
723  if (xfwhm < 0) {
724  cpl_table_set_invalid(measurements, "xfwhm", irow);
725  }
726  if (yfwhm < 0) {
727  cpl_table_set_invalid(measurements, "yfwhm", irow);
728  }
729  cpl_table_dump(measurements, irow, 1, stdout);
730  fflush(stdout);
731  }
732 #endif
733  irow++;
734  } /* for naper (all spot apertures) */
735  } /* for k (all images in list) */
736  cpl_apertures_delete(apertures);
737  cpl_free(aperidx);
738  muse_trace_polys_delete(ptrace);
739  } /* for nslice */
740  } /* for iline */
741 
742  /* clean up unused rows */
743  cpl_table_erase_invalid(measurements);
744 
745  /* sort table to get spots close together */
746  cpl_propertylist *order = cpl_propertylist_new();
747  cpl_propertylist_append_bool(order, "lambda", CPL_FALSE);
748  cpl_propertylist_append_bool(order, "SliceCCD", CPL_FALSE);
749  cpl_propertylist_append_bool(order, "SpotNo", CPL_FALSE);
750  cpl_propertylist_append_bool(order, "VPOS", CPL_FALSE);
751  cpl_table_sort(measurements, order);
752  cpl_propertylist_delete(order);
753 
754  return measurements;
755 } /* muse_geo_measure_spots() */
756 
757 /*----------------------------------------------------------------------------*/
793 /*----------------------------------------------------------------------------*/
794 static cpl_table *
795 muse_geo_get_spot_peaks(cpl_table *aSpots, unsigned char aIFU,
796  unsigned short aNSlice, unsigned char aNSpot,
797  double aLambda, double aVPosRef, cpl_boolean aVerifyDY,
798  cpl_array *aDY)
799 {
800  if (!aSpots) { /* return without raising an error */
801  return NULL;
802  }
803 
804  /* This is not very efficient (one could use the sortedness of the *
805  * table for a more clever selection algorithm) but no bottleneck, *
806  * so leave it like this for the moment. */
807  cpl_table_unselect_all(aSpots);
808  cpl_size irow, nrow = cpl_table_get_nrow(aSpots);
809  for (irow = 0; irow < nrow; irow++) {
810  if (cpl_table_get_int(aSpots, "SliceCCD", irow, NULL) == aNSlice &&
811  cpl_table_get_int(aSpots, "SpotNo", irow, NULL) == aNSpot &&
812  cpl_table_get_double(aSpots, "lambda", irow, NULL) == aLambda) {
813  cpl_table_select_row(aSpots, irow);
814  }
815  } /* for irow */
816  cpl_size nextracted = cpl_table_count_selected(aSpots);
817  if (nextracted < 1) { /* no entries for this combination */
818  /* this means that we cannot do the analysis for this slice at *
819  * this wavelength at all, skip the rest of the spots, too */
820  cpl_msg_debug(__func__, "No detection for spot %1hhu in slice %2hu of IFU "
821  "%hhu at wavelength %.3f", aNSpot, aNSlice, aIFU, aLambda);
822  return NULL;
823  }
824  cpl_table *tspot = cpl_table_extract_selected(aSpots);
825 #if 0
826  printf("tspot:\n");
827  cpl_table_dump(tspot, 0, 10000, stdout);
828  fflush(stdout);
829 #endif
830  /* convert the "flux" table column into an image and detect peaks */
831  int nsrow = cpl_table_get_nrow(tspot);
832  cpl_image *imflux = cpl_image_wrap(nsrow, 1,
833  CPL_TYPE_DOUBLE,
834  cpl_table_get_data_double(tspot, "flux"));
835  /* create a mask of the fiducial peaks and filter (dilate) *
836  * the mask to make sure to include enough of the flux */
837  cpl_stats *s = cpl_stats_new_from_image(imflux,
838  CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV);
839  double limit = cpl_stats_get_median(s) + cpl_stats_get_median_dev(s) * 0.5;
840  cpl_stats_delete(s);
841  if (limit > 500.) {
842  limit = 500.;
843  }
844  cpl_mask *mask = cpl_mask_threshold_image_create(imflux, limit, DBL_MAX);
845  cpl_mask *kernel = cpl_mask_new(3, 1);
846  cpl_mask_not(kernel);
847  cpl_mask *mask2 = cpl_mask_duplicate(mask);
848  cpl_mask_filter(mask, mask2, kernel, CPL_FILTER_DILATION, CPL_BORDER_NOP);
849  cpl_mask_delete(mask2);
850  cpl_mask_delete(kernel);
851  cpl_apertures *aper = cpl_apertures_extract_mask(imflux, mask);
852  cpl_mask_delete(mask);
853  if (!aper) {
854  cpl_msg_warning(__func__, "No detection for spot %1hhu in slice %2hu of IFU "
855  "%2hhu at wavelength %.3f", aNSpot, aNSlice, aIFU, aLambda);
856  cpl_table_delete(tspot);
857  cpl_image_unwrap(imflux);
858  return NULL; /* no analysis possible, skip spots in this slice at this wavelength */
859  }
860  /* take aperture closest to the image center, but only if it's not on the border */
861  double dcenter = DBL_MAX;
862  int naper, ndcenter = -1;
863  for (naper = 1; naper <= cpl_apertures_get_size(aper); naper++) {
864  /* exclude apertures with too few positions */
865  int npos = cpl_apertures_get_npix(aper, naper);
866  if (cpl_apertures_get_size(aper) > 1 && npos < 3) {
867  cpl_msg_debug(__func__, "ifu %2hhu sliceccd %2d spot %1hhu lambda %.3f, "
868  "aperture %d: only %d positions -> skip", aIFU, aNSlice,
869  aNSpot, aLambda, naper, npos);
870  continue;
871  }
872  /* start x-reference at the VPOS values of the middle table row */
873  double xref = aVPosRef > 0 ? aVPosRef
874  : cpl_table_get_double(tspot, "VPOS", (nsrow + 1) / 2, NULL),
875  xcentroid = cpl_apertures_get_centroid_x(aper, naper);
876  /* interpolate the corresponding vpos value of this xcentroid, *
877  * take into account that there might be missing entries! */
878  irow = 0;
879  while (++irow + 1 < xcentroid) ;
880  double pp1 = cpl_table_get_double(tspot, "VPOS", irow - 1, NULL),
881  pp2 = cpl_table_get_double(tspot, "VPOS", irow, NULL),
882  ppfrac = xcentroid - irow;
883 #if 0
884  cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" (%f) --> %f %f ==> %f", irow,
885  xcentroid, pp1, pp2, pp1 * (1 - ppfrac) + pp2 * ppfrac);
886 #endif
887  double ppcentroid = pp1 * (1 - ppfrac) + pp2 * ppfrac;
888  /* now computed distance to the center of the previous spot */
889  double dc = fabs(ppcentroid - xref);
890  int x1 = cpl_apertures_get_left(aper, naper),
891  x2 = cpl_apertures_get_right(aper, naper);
892  if (dc < dcenter && x1 > 1 && x2 < nsrow) {
893  dcenter = dc;
894  ndcenter = naper;
895  } /* if */
896  } /* for naper */
897 
898  /* derive the vertical pinhole distance (which only nominally is 0.6135 mm) */
899  if (aDY || aVerifyDY) {
900  for (naper = 1; naper < cpl_apertures_get_size(aper); naper++) {
901  int l1 = cpl_apertures_get_left(aper, naper),
902  r1 = cpl_apertures_get_right(aper, naper),
903  l2 = cpl_apertures_get_left(aper, naper + 1),
904  r2 = cpl_apertures_get_right(aper, naper + 1);
905  if (l1 > 1 && r1 < nsrow && l2 > 1 && r2 < nsrow) {
906  /* compute peak centroids for both selected apertures */
907  double peak[2];
908  int n2aper;
909  for (n2aper = naper; n2aper <= naper + 1; n2aper++) {
910  cpl_size irow1 = cpl_apertures_get_left(aper, n2aper) - 1,
911  irow2 = cpl_apertures_get_right(aper, n2aper) - 1;
912  double vpos = 0., ftot = 0.;
913  for (irow = irow1; irow <= irow2; irow++) {
914  double flux = cpl_table_get(tspot, "flux", irow, NULL);
915  ftot += flux;
916  vpos += cpl_table_get(tspot, "VPOS", irow, NULL) * flux;
917  } /* for irow */
918  peak[n2aper - naper] = vpos / ftot;
919  } /* for n2aper */
920  double xcdiff = fabs(peak[1] - peak[0]);
921  if (aDY) { /* record the peak distance in the in/output array */
922  /* set up index for writing into the aDY array */
923  cpl_errorstate state = cpl_errorstate_get();
924  cpl_size idy = 0, ndy = cpl_array_get_size(aDY);
925  while (cpl_array_is_valid(aDY, idy) > 0) { /* skip all valid entries */
926  idy++;
927  }
928  if (cpl_array_get_size(aDY) <= idy) {
929  cpl_array_set_size(aDY, ndy * 1.5);
930  cpl_errorstate_set(state);
931  }
932 #if 0
933  cpl_msg_debug(__func__, "xcdiff = %f (%f - %f = %f) / index %"CPL_SIZE_FORMAT,
934  xcdiff, peak[1], peak[0], peak[1] - peak[0], idy);
935 #endif
936  cpl_array_set_double(aDY, idy, xcdiff);
937  } /* if aDY */
938  if (aVerifyDY) {
939  printf("\"centroids_d_%f.dat\" u 18:16 t \"d %f (%f %f)\" w lp, \\\n",
940  xcdiff, xcdiff, peak[0], peak[1]);
941  char *fn = cpl_sprintf("centroids_d_%f.dat", xcdiff);
942  FILE *fp = fopen(fn, "w");
943  fprintf(fp, "# good centroids at %f and %f --> d = %f mm\n#", peak[0], peak[1], xcdiff);
944  cpl_table_dump(tspot, 0, 10000, fp);
945  fflush(fp);
946  fclose(fp);
947  cpl_free(fn);
948  } /* if aVerifyDY */
949  } /* if non-border apertures */
950  } /* for naper (all but last apertures) */
951  } /* if dy verification required */
952  if (ndcenter < 1) { /* didn't find a suitable aperture */
953  cpl_msg_warning(__func__, "Motion of spot %1hhu in slice %2hu of IFU "
954  "%2hhu at wavelength %.3f did not result in usable "
955  "coverage", aNSpot, aNSlice, aIFU, aLambda);
956  cpl_table_delete(tspot);
957  cpl_apertures_delete(aper);
958  cpl_image_unwrap(imflux);
959  return NULL; /* no analysis possible, skip spots in this slice at this wavelength */
960  }
961  cpl_size irow1 = cpl_apertures_get_left(aper, ndcenter) - 1,
962  irow2 = cpl_apertures_get_right(aper, ndcenter) - 1;
963  cpl_apertures_delete(aper);
964  cpl_image_unwrap(imflux);
965 
966  /* select the relevant table rows*/
967  cpl_table_unselect_all(tspot);
968  for (irow = irow1; irow <= irow2; irow++) {
969  cpl_table_select_row(tspot, irow);
970  } /* for irow */
971  cpl_table *result = cpl_table_extract_selected(tspot);
972  cpl_table_delete(tspot);
973  return result;
974 } /* muse_geo_get_spot_peaks() */
975 
976 /*----------------------------------------------------------------------------*/
1009 /*----------------------------------------------------------------------------*/
1010 cpl_error_code
1011 muse_geo_compute_pinhole_local_distance(cpl_array *aDY, cpl_table *aSpots)
1012 {
1013  cpl_ensure_code(aDY && aSpots, CPL_ERROR_NULL_INPUT);
1014  cpl_ensure_code(cpl_array_get_type(aDY) == CPL_TYPE_DOUBLE,
1015  CPL_ERROR_INCOMPATIBLE_INPUT);
1016  cpl_size nrow = cpl_table_get_nrow(aSpots);
1017  cpl_ensure_code(nrow > 10, CPL_ERROR_ILLEGAL_INPUT);
1018  cpl_ensure_code(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
1019  CPL_ERROR_INCOMPATIBLE_INPUT);
1020  const unsigned char ifu = cpl_table_get_column_min(aSpots, "SubField"),
1021  ifu2 = cpl_table_get_column_max(aSpots, "SubField");
1022  cpl_ensure_code(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1023  CPL_ERROR_ILLEGAL_INPUT);
1024  cpl_ensure_code(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
1025  CPL_ERROR_ILLEGAL_INPUT);
1026 
1027  cpl_boolean verifydy = getenv("MUSE_DEBUG_GEO_VERIFY_DY")
1028  && atoi(getenv("MUSE_DEBUG_GEO_VERIFY_DY")) > 0;
1029  if (verifydy) {
1030  cpl_msg_warning(__func__, "Running with DY pinhole distance verification on"
1031  " (MUSE_DEBUG_GEO_VERIFY_DY=%s), will produce lots of files "
1032  "\"centroids_d_*.dat\"!", getenv("MUSE_DEBUG_GEO_VERIFY_DY"));
1033  }
1034 
1035  /* extract a list of unique wavelengths from the spots table */
1036  double *lbda = cpl_table_get_data_double(aSpots, "lambda");
1037  cpl_vector *vlbda = cpl_vector_wrap(nrow, lbda);
1038  cpl_vector *lambdas = muse_cplvector_get_unique(vlbda);
1039  cpl_vector_unwrap(vlbda);
1040  int nlines = cpl_vector_get_size(lambdas);
1041 
1042  /* create array with likely size that may be needed *
1043  * (it will be enlarged or or trimmed as required) */
1044  cpl_array *dy = cpl_array_new(kMuseSlicesPerCCD * nlines * kMuseCUmpmSpotsPerSlice,
1045  CPL_TYPE_DOUBLE);
1046 
1047  /* loop through the table, do statistics on each spot *
1048  * and record the vertical pinhole position difference *
1049  * for each doubly illuminated slice */
1050  unsigned short nslice;
1051  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1052  int iline;
1053  for (iline = 0; iline < nlines; iline++) {
1054  double lambda = cpl_vector_get(lambdas, iline);
1055 
1056  unsigned char nspot;
1057  double vposref = -DBL_MAX;
1058  for (nspot = 1; nspot <= kMuseCUmpmSpotsPerSlice; nspot++) {
1059  cpl_table *tspot = muse_geo_get_spot_peaks(aSpots, ifu, nslice, nspot,
1060  lambda, vposref, verifydy, dy);
1061  cpl_table_delete(tspot);
1062  } /* for nspot (all spots) */
1063  } /* for iline (all wavelengths) */
1064  } /* for nslice (all slices) */
1065  cpl_vector_delete(lambdas);
1066  /* now resize the array to the real size needed and append it to the input array */
1068  cpl_msg_debug(__func__, "Median vertical pinhole distance in IFU %02hhu: %f mm",
1069  ifu, cpl_array_get_median(dy));
1070  #pragma omp critical (geo_dy_array_insert)
1071  cpl_array_insert(aDY, dy, cpl_array_get_size(aDY));
1072  cpl_array_delete(dy);
1073 
1074  return CPL_ERROR_NONE;
1075 } /* muse_geo_compute_pinhole_local_distance() */
1076 
1077 /*----------------------------------------------------------------------------*/
1105 /*----------------------------------------------------------------------------*/
1106 double
1107 muse_geo_compute_pinhole_global_distance(cpl_array *aDY, double aWidth,
1108  double aMin, double aMax)
1109 {
1110  cpl_ensure(aDY, CPL_ERROR_NULL_INPUT, 0.);
1111  cpl_ensure(cpl_array_get_type(aDY) == CPL_TYPE_DOUBLE,
1112  CPL_ERROR_INCOMPATIBLE_INPUT, 0.);
1113  cpl_ensure(cpl_array_count_invalid(aDY) < cpl_array_get_size(aDY),
1114  CPL_ERROR_ILLEGAL_INPUT, 0.);
1115 
1116  /* create a histogram and clean it */
1117  cpl_bivector *histogram = muse_cplarray_histogram(aDY, aWidth, aMin, aMax);
1118 #if 0
1119  printf("aDY array 1: %f +/- %f (%f)\n", cpl_array_get_mean(aDY),
1120  cpl_array_get_stdev(aDY), cpl_array_get_median(aDY));
1121  cpl_array_dump(aDY, 0, 1000000, stdout);
1122  printf("aDY histogram 1:\n");
1123  cpl_plot_bivector(NULL, "w lp", NULL, histogram);
1124  cpl_bivector_dump(histogram, stdout);
1125  fflush(stdout);
1126 #endif
1127  muse_cplarray_erase_outliers(aDY, histogram, 1, 0.5);
1128  cpl_bivector_delete(histogram);
1129  double mean = cpl_array_get_mean(aDY),
1130  stdev = cpl_array_get_stdev(aDY),
1131  min = mean - 2 * stdev,
1132  max = mean + 2 * stdev,
1133  step = (max - min) / 20.;
1134 #if 0
1135  double median = cpl_array_get_median(aDY);
1136  printf("aDY array 2: %f +/- %f (%f)\n", mean, stdev, median);
1137  cpl_array_dump(aDY, 0, 1000000, stdout);
1138  fflush(stdout);
1139 #endif
1140  histogram = muse_cplarray_histogram(aDY, step, min, max);
1141 #if 0
1142  printf("aDY histogram 2:\n");
1143  cpl_plot_bivector(NULL, "w lp", NULL, histogram);
1144  cpl_bivector_dump(histogram, stdout);
1145  fflush(stdout);
1146 #endif
1147  muse_cplarray_erase_outliers(aDY, histogram, 1, 0.5);
1148  cpl_bivector_delete(histogram);
1149  mean = cpl_array_get_mean(aDY);
1150  stdev = cpl_array_get_stdev(aDY);
1151 #if 0
1152  double median2 = cpl_array_get_median(aDY);
1153  printf("aDY array 3: %f +/- %f (%f)\n", mean, stdev, median2);
1154  cpl_array_dump(aDY, 0, 1000000, stdout);
1155  fflush(stdout);
1156 #endif
1157 
1158  /* print output and set the computed value in the environment, *
1159  * if the variable does not already exist */
1160  cpl_msg_info(__func__, "Computed vertical pinhole distance of %.6f +/- %.6f "
1161  "mm (instead of %.4f)", mean, stdev, kMuseCUmpmDY);
1162  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1163  cpl_msg_warning(__func__, "Vertical pinhole distance already overridden in the "
1164  "environment (%f mm)", atof(getenv("MUSE_GEOMETRY_PINHOLE_DY")));
1165  } else {
1166  char *envstring = cpl_sprintf("%f", mean);
1167  int err = setenv("MUSE_GEOMETRY_PINHOLE_DY", envstring, 1);
1168  if (!err) {
1169  cpl_msg_info(__func__, "Set MUSE_GEOMETRY_PINHOLE_DY=%s in the environment",
1170  envstring);
1171  }
1172  cpl_free(envstring);
1173  }
1174 
1175  return mean;
1176 } /* muse_geo_compute_pinhole_global_distance() */
1177 
1178 /*----------------------------------------------------------------------------*/
1189 /*----------------------------------------------------------------------------*/
1191 muse_geo_table_new(cpl_size aNRows, double aScale)
1192 {
1193  muse_geo_table *gt = cpl_calloc(1, sizeof(muse_geo_table));
1194  gt->table = muse_cpltable_new(muse_geo_table_def, aNRows);
1195  gt->scale = aScale;
1196  return gt;
1197 }
1198 
1199 /*----------------------------------------------------------------------------*/
1210 /*----------------------------------------------------------------------------*/
1213 {
1214  cpl_ensure(aGeo, CPL_ERROR_NULL_INPUT, NULL);
1215  muse_geo_table *gt = cpl_calloc(1, sizeof(muse_geo_table));
1216  gt->table = cpl_table_duplicate(aGeo->table);
1217  gt->scale = aGeo->scale;
1218  return gt;
1219 }
1220 
1221 /*----------------------------------------------------------------------------*/
1230 /*----------------------------------------------------------------------------*/
1231 void
1233 {
1234  if (!aGeo) {
1235  return;
1236  }
1237  cpl_table_delete(aGeo->table);
1238  aGeo->table = NULL;
1239  cpl_free(aGeo);
1240 }
1241 
1242 /*----------------------------------------------------------------------------*/
1306 /*----------------------------------------------------------------------------*/
1308 muse_geo_determine_initial(cpl_table *aSpots, const cpl_table *aTrace)
1309 {
1310  cpl_ensure(aSpots && aTrace, CPL_ERROR_NULL_INPUT, NULL);
1311  cpl_size nrow = cpl_table_get_nrow(aSpots);
1312  cpl_ensure(nrow > 10, CPL_ERROR_ILLEGAL_INPUT, NULL);
1313  cpl_ensure(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
1314  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
1315  const unsigned char ifu = cpl_table_get_column_min(aSpots, "SubField"),
1316  ifu2 = cpl_table_get_column_max(aSpots, "SubField");
1317  cpl_ensure(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1318  CPL_ERROR_ILLEGAL_INPUT, NULL);
1319  const double kScale = cpl_table_get_column_mean(aSpots, "ScaleFOV");
1320  cpl_ensure(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
1321  CPL_ERROR_ILLEGAL_INPUT, NULL);
1322 
1323  cpl_boolean noinvertangle = getenv("MUSE_GEOMETRY_NO_INVERT_ANGLE")
1324  && atoi(getenv("MUSE_GEOMETRY_NO_INVERT_ANGLE")) > 0;
1325  double maskangle = 0., fmaskrot = 1.;
1326  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
1327  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
1328  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
1329  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
1330  "(cos = %.4e)", maskangle, fmaskrot);
1331  }
1332  double pinholedy = kMuseCUmpmDY;
1333  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1334  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
1335  cpl_msg_warning(__func__, "Using pinhole y distance of %f mm (instead of "
1336  "%f mm)", pinholedy, kMuseCUmpmDY);
1337  }
1338 
1339  /* extract a list of unique wavelengths from the spots table */
1340  double *lbda = cpl_table_get_data_double(aSpots, "lambda");
1341  cpl_vector *vlbda = cpl_vector_wrap(nrow, lbda);
1342  cpl_vector *lambdas = muse_cplvector_get_unique(vlbda);
1343  cpl_vector_unwrap(vlbda);
1344  int nlines = cpl_vector_get_size(lambdas);
1345  muse_geo_table *gt = muse_geo_table_new(kMuseSlicesPerCCD
1346  * kMuseCUmpmSpotsPerSlice * nlines,
1347  kScale);
1348  /* loop through the table, do statistics on each spot (position vs flux) */
1349  cpl_size irrow = 0; /* row in gt->table */
1350  unsigned short nslice;
1351  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1352  cpl_polynomial **ptrace = muse_trace_table_get_polys_for_slice(aTrace,
1353  nslice);
1354  if (!ptrace) {
1355  continue;
1356  }
1357  int iline;
1358  for (iline = 0; iline < nlines; iline++) {
1359  double lambda = cpl_vector_get(lambdas, iline);
1360 
1361  unsigned char nspot, nslicespot = 0;
1362  double vposref = -DBL_MAX;
1363  for (nspot = 1; nspot <= kMuseCUmpmSpotsPerSlice; nspot++) {
1364  cpl_table *tspot = muse_geo_get_spot_peaks(aSpots, ifu, nslice, nspot,
1365  lambda, vposref, CPL_FALSE,
1366  NULL);
1367  if (!tspot) {
1368  break;
1369  }
1370  nslicespot++;
1371  double xcenter = 0., ycenter = 0., /* properties for weighted centroids */
1372  vpos = 0., ftot = 0.;
1373  nrow = cpl_table_get_nrow(tspot);
1374  cpl_size irow;
1375  for (irow = 0; irow < nrow; irow++) {
1376  double flux = cpl_table_get(tspot, "flux", irow, NULL);
1377  ftot += flux;
1378  xcenter += cpl_table_get(tspot, "xc", irow, NULL) * flux;
1379  ycenter += cpl_table_get(tspot, "yc", irow, NULL) * flux;
1380  vpos += cpl_table_get(tspot, "VPOS", irow, NULL) * flux;
1381  } /* for irow (tspot table rows) */
1382  cpl_table_delete(tspot);
1383  if (ftot <= 0.) {
1384  cpl_msg_warning(__func__, "Invalid integrated flux of spot %1hhu/%1hhu "
1385  "in slice %2hu of IFU %2hhu at wavelength %.3f: %e",
1386  nspot, nslicespot, nslice, ifu, lambda, ftot);
1387  break;
1388  }
1389  xcenter /= ftot;
1390  ycenter /= ftot;
1391  vpos /= ftot;
1392  if (vposref < 0) {
1393  /* remember the peak center of this spot as reference for the others */
1394  vposref = vpos;
1395  }
1396  double xcen = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_CENTER], ycenter,
1397  NULL),
1398  xl = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_LEFT], ycenter, NULL),
1399  xr = cpl_polynomial_eval_1d(ptrace[MUSE_TRACE_RIGHT], ycenter, NULL),
1400  xwidth = xr - xl;
1401  cpl_msg_info(__func__, "ifu %2hhu sliceccd %2d spot %1hhu/%1hhu lambda %.3f "
1402  "x/y %8.3f %8.3f (xcen %8.3f xwidth %6.3f) vpos %f flux %e",
1403  ifu, nslice, nspot, nslicespot, lambda, xcenter, ycenter,
1404  xcen, xwidth, vpos, ftot);
1405  cpl_table_set_int(gt->table, MUSE_GEOTABLE_FIELD, irrow, ifu);
1406  cpl_table_set_int(gt->table, MUSE_GEOTABLE_SKY, irrow,
1407  kMuseGeoSliceSky[nslice - 1]);
1408  cpl_table_set_int(gt->table, MUSE_GEOTABLE_CCD, irrow, nslice);
1409  cpl_table_set_int(gt->table, "spot", irrow, nslicespot);
1410  /* follow the optical numbering, with "reversed" numbering, *
1411  * see VLT-TRE-MUS-14670-0657 v1.06, Sect. 1.4.2.2.3 */
1412  unsigned char stack = nslice <= 12 ? 4 : (nslice <= 24 ? 3 : (nslice <= 36 ? 2 : 1));
1413  cpl_table_set_int(gt->table, "stack", irrow, stack);
1414  /* don't set "spot" column here, see below */
1415  cpl_table_set_double(gt->table, "xc", irrow, xcenter);
1416  cpl_table_set_double(gt->table, "yc", irrow, ycenter);
1417  cpl_table_set_double(gt->table, "dxl", irrow, xcenter - xl);
1418  cpl_table_set_double(gt->table, "dxr", irrow, xr - xcenter);
1419  cpl_table_set_double(gt->table, "vpos", irrow, vpos);
1420  cpl_table_set_double(gt->table, "flux", irrow, ftot);
1421  cpl_table_set_double(gt->table, "lambda", irrow, lambda);
1422  /* set width and angle columns invalid at the beginning, *
1423  * to be able to select invalid table entries */
1424  cpl_table_set_invalid(gt->table, MUSE_GEOTABLE_WIDTH, irrow);
1425  cpl_table_set_invalid(gt->table, MUSE_GEOTABLE_ANGLE, irrow);
1426 
1427  /* use data of all three spots in this slice to compute *
1428  * relative width, angle, and horizontal position */
1429  if (nslicespot == kMuseCUmpmSpotsPerSlice) {
1430  /* get data of the previous two spots back from the table */
1431  int err1a, err2a, err1b, err2b;
1432  double xc1 = cpl_table_get_double(gt->table, "xc", irrow - 2, &err1a),
1433  xc2 = cpl_table_get_double(gt->table, "xc", irrow - 1, &err2a),
1434  vpos1 = cpl_table_get_double(gt->table, "vpos", irrow - 2, &err1b),
1435  vpos2 = cpl_table_get_double(gt->table, "vpos", irrow - 1, &err2b);
1436  if (!err1a && !err2a) {
1437  if (xcenter < xc2 || xc2 < xc1) {
1438  cpl_msg_warning(__func__, "spots are not sorted left-to-right on "
1439  "the CCD (%f .. %f .. %f)!", xc1, xc2, xcenter);
1440  }
1441  double dx = (xcenter - xc1) / 2.;
1442  /* a kind of standard deviation of the three values: *
1443  * dx, dx1, dx2; the term for dx itself is always zero */
1444  double dxerr = sqrt((pow(xcenter - xc2 - dx, 2)
1445  + pow(xc2 - xc1 - dx, 2)) / 3.);
1446  /* See if the error is high, as this means that one of the three *
1447  * positions and hence of the two distances is likely wrong. *
1448  * Then use an estimate of what the distance should really be to *
1449  * decide which of the two distances is correct. */
1450  /* XXX this is ugly, as dxexpected may be a bad estimate, *
1451  * derived using the data of April 2013 with only 6 IFUs */
1452  double xreloffset = 0.;
1453  if (dxerr > 0.3) {
1454  const double dxexpected = 26.04644 - 0.05537208 * ifu;
1455  double dx1 = xc2 - xc1,
1456  dx2 = xcenter - xc2,
1457  diff1 = fabs(dx1 - dxexpected),
1458  diff2 = fabs(dx2 - dxexpected),
1459  dxnew;
1460  if (fmin(diff1, diff2) > 1.5) { /* more than 1.5 pix difference! */
1461  dxnew = 99.; /* set to large value to trigger the bad width case below */
1462  } else if (diff2 > diff1) {
1463  dxnew = dx1;
1464  } else {
1465  dxnew = dx2;
1466  }
1467  /* correct the xrel to be computed below, too, by half *
1468  * the difference in width that this change makes */
1469  xreloffset = xwidth * kMuseCUmpmDX / fmaskrot
1470  * kMuseTypicalCubeSizeX * kScale / 60.
1471  * fabs(1. / dx - 1 / dxnew) / 2.;
1472  cpl_msg_warning(__func__, "ifu %2hhu sliceccd %2d spot %1hhu/%1hhu "
1473  "lambda %.3f dx %.3f +/- %.3f (%.3f %.3f): dxerr "
1474  "is large, using a guess of %.3f +/- %.3f "
1475  "(expected was %.3f for this IFU), adding %.3f to"
1476  " xrel!", ifu, nslice, nspot, nslicespot, lambda,
1477  dx, dxerr, xc2-xc1, xcenter-xc2, dxnew, dxerr / 2.,
1478  dxexpected, xreloffset);
1479  dx = dxnew;
1480  dxerr /= 2.; /* hopefully we are not a factor of two less wrong! */
1481  } /* if dxerr > 0.3 */
1482  cpl_table_fill_column_window_double(gt->table, "dx", irrow - 2, 3, dx);
1483  cpl_table_fill_column_window_double(gt->table, "dxerr", irrow - 2, 3, dxerr);
1484  /* Project the size back to the focal plane and use the known *
1485  * scales to compute the effective slice width in nominal pixels. *
1486  * Strictly, one should take the slice width from the trace at the *
1487  * center of the three y values, but the difference is negligible. */
1488  double scale = kMuseCUmpmDX / fmaskrot / dx, /* scale of this slice [mm / pix] */
1489  width = xwidth * scale * kMuseTypicalCubeSizeX * kScale / 60.,
1490  werr = width * dxerr / dx; /* error estimate */
1491  if (width > kMuseSliceLoLikelyWidth && width < kMuseSliceHiLikelyWidth) {
1492  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_WIDTH,
1493  irrow - 2, 3, width);
1494  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_WIDTH"err",
1495  irrow - 2, 3, werr);
1496  } else {
1497  cpl_msg_warning(__func__, "ifu %2hhu slice %2d lambda %.3f: computed "
1498  "an unlikely width: %.3f +/- %.3f", ifu, nslice,
1499  lambda, width, werr);
1500  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_WIDTH,
1501  irrow - 2, 3);
1502  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_WIDTH"err",
1503  irrow - 2, 3);
1504  }
1505 
1506  /* now compute relative horizontal positions against the center *
1507  * of the slice, in mm in the focal plane, for all three spots */
1508  double xrel = (xcen - xc1 + xreloffset) * scale,
1509  xrelerr = fabs(xrel * dxerr / dx);
1510  cpl_table_set_double(gt->table, "xrel", irrow - 2, xrel);
1511  cpl_table_set_double(gt->table, "xrelerr", irrow - 2, xrelerr);
1512  xrel = (xcen - xc2 + xreloffset) * scale;
1513  xrelerr = fabs(xrel * dxerr / dx);
1514  cpl_table_set_double(gt->table, "xrel", irrow - 1, xrel);
1515  cpl_table_set_double(gt->table, "xrelerr", irrow - 1, xrelerr);
1516  xrel = (xcen - xcenter + xreloffset) * scale;
1517  xrelerr = fabs(xrel * dxerr / dx);
1518  cpl_table_set_double(gt->table, "xrel", irrow, xrel);
1519  cpl_table_set_double(gt->table, "xrelerr", irrow, xrelerr);
1520  } /* else: got all x-pos measurements */
1521 
1522  if (!err1b && !err2b) { /* compute angle in degrees */
1523  /* check if the vpos values correspond to the same flux peak or *
1524  * if we need to fix them up for the vertical pinhole distance */
1525  double pdiff = fmax(vpos1, fmax(vpos2, vpos))
1526  - fmin(vpos1, fmin(vpos2, vpos));
1527  if (pdiff > 0.5) { /* max diff. 0.5 arcsec =~ 0.3 mm */
1528  double pmean = (vpos1 + vpos2 + vpos) / 3.;
1529  if (vpos1 > pmean) { /* subtract pinhole distance from high values */
1530  vpos1 -= pinholedy * fmaskrot;
1531  }
1532  if (vpos2 > pmean) {
1533  vpos2 -= pinholedy * fmaskrot;
1534  }
1535  if (vpos > pmean) {
1536  vpos -= pinholedy * fmaskrot;
1537  }
1538  double pdiff2 = fmax(vpos1, fmax(vpos2, vpos))
1539  - fmin(vpos1, fmin(vpos2, vpos));
1540  if (pdiff2 < pdiff) {
1541  cpl_msg_debug(__func__, "Fixed max vpos diff from %f down to %f",
1542  pdiff, pdiff2);
1543  } else { /* didn't work, copy the old values */
1544  double vpos1o = cpl_table_get_double(gt->table, "vpos", irrow - 2, NULL),
1545  vpos2o = cpl_table_get_double(gt->table, "vpos", irrow - 1, NULL),
1546  vpos3o = cpl_table_get_double(gt->table, "vpos", irrow, NULL);
1547  cpl_msg_warning(__func__, "Large max vpos diff detected but "
1548  "not fixed! (original: %f %f %f, %f -> mean %f "
1549  "-> fixed: %f %f %f, %f)", vpos1o, vpos2o, vpos3o,
1550  pdiff, pmean, vpos1, vpos2, vpos, pdiff2);
1551  vpos1 = vpos1o;
1552  vpos2 = vpos2o;
1553  vpos = vpos3o;
1554  }
1555  } /* if large vpos differences */
1556 
1557  double f = 1. / kMuseCUmpmDX / fmaskrot, /* 1 / distance */
1558  angle1 = atan((vpos2 - vpos1) * f) * CPL_MATH_DEG_RAD,
1559  angle2 = atan((vpos - vpos2) * f) * CPL_MATH_DEG_RAD,
1560  angle = (angle1 + angle2) / 2.;
1561  if (!noinvertangle) { /* usually, the angles need to be reversed */
1562  angle1 *= -1.;
1563  angle2 *= -1.;
1564  angle *= -1.;
1565  }
1566  /* simple-minded standard deviation again */
1567  double aerr = sqrt((pow(angle1 - angle, 2) + pow(angle2 - angle, 2)) / 3.);
1568  if (fabs(angle) < kMuseGeoSliceMaxAngle) {
1569  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_ANGLE,
1570  irrow - 2, 3, angle);
1571  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_ANGLE"err",
1572  irrow - 2, 3, aerr);
1573  } else {
1574  cpl_msg_warning(__func__, "ifu %2hhu slice %2d lambda %.3f: computed "
1575  "an unlikely angle: %.3f +/- %.3f", ifu, nslice,
1576  lambda, angle, aerr);
1577  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_ANGLE,
1578  irrow - 2, 3);
1579  cpl_table_set_column_invalid(gt->table, MUSE_GEOTABLE_ANGLE"err",
1580  irrow - 2, 3);
1581  }
1582  } /* else: got all vpos measurements */
1583 
1584  /* and get the dxl value of the left-most spot and the dxr *
1585  * value of the right-most spot and set them for all spots */
1586  double dx = cpl_table_get_double(gt->table, "dx", irrow - 1, NULL),
1587  dxl = cpl_table_get_double(gt->table, "dxl", irrow - 2, NULL),
1588  dxr = cpl_table_get_double(gt->table, "dxr", irrow, NULL);
1589  cpl_table_fill_column_window_double(gt->table, "dxl", irrow - 2, 3, dxl / dx);
1590  cpl_table_fill_column_window_double(gt->table, "dxr", irrow - 2, 3, dxr / dx);
1591  } /* if nslicespot == kMuseCUmpmSpotsPerSlice (last spot in slice) */
1592 
1593  irrow++;
1594  } /* for nspot (all spots) */
1595  } /* for iline (all wavelengths) */
1596  muse_trace_polys_delete(ptrace);
1597  } /* for nslice (all slices) */
1598  cpl_vector_delete(lambdas);
1599  cpl_table_set_size(gt->table, irrow); /* remove empty rows */
1600  /* erase rows which only got a partial analysis (the |break|s above) */
1601  cpl_table_and_selected_invalid(gt->table, MUSE_GEOTABLE_WIDTH);
1602  cpl_table_or_selected_invalid(gt->table, MUSE_GEOTABLE_ANGLE);
1603  cpl_table_erase_selected(gt->table);
1604  return gt;
1605 } /* muse_geo_determine_initial() */
1606 
1607 /*----------------------------------------------------------------------------*/
1627 /*----------------------------------------------------------------------------*/
1628 static cpl_size
1629 muse_geo_determine_horizontal_wmean(const cpl_table *aSlice,
1630  const char *aCol, const char *aColErr,
1631  double *aValue, double *aError,
1632  double *aMedian, double aSigma)
1633 {
1634  const char func[] = "muse_geo_determine_horizontal"; /* pretend to be there */
1635  if (!aSlice) return -1;
1636  if (!aCol || !aValue) return -1;
1637 
1638  /* first create vector(s) from the relevant table columns */
1639  const double *vv = cpl_table_get_data_double_const(aSlice, aCol),
1640  *ve = NULL;
1641  if (aColErr) {
1642  ve = cpl_table_get_data_double_const(aSlice, aColErr);
1643  }
1644  if (!vv) return -1;
1645  /* compute median and median absolute deviation to get starting limits */
1646  cpl_size n = cpl_table_get_nrow(aSlice);
1647  cpl_vector *vtmp = cpl_vector_wrap(n, (double *)cpl_table_get_data_double_const(aSlice, aCol));
1648  double median = cpl_table_get_column_median(aSlice, aCol),
1649  mdev = muse_cplvector_get_adev_const(vtmp, median),
1650  vlo = median - aSigma * mdev,
1651  vhi = median + aSigma * mdev;
1652  cpl_vector_unwrap(vtmp);
1653  cpl_msg_debug(func, "%s/%s: median %f +/- %f --> %f...%f", aCol, aColErr,
1654  median, mdev, vlo, vhi);
1655 
1656  /* iterate: compute weighted mean, using only entries *
1657  * within the computed boundaries */
1658  double value, sigma = mdev,
1659  min, max;
1660  cpl_vector *vmedian = NULL;
1661  cpl_size nrejected;
1662  do {
1663  min = DBL_MAX;
1664  max = -DBL_MAX;
1665  if (aMedian) {
1666  vmedian = cpl_vector_new(n);
1667  }
1668  double weight = 0.;
1669  value = 0.;
1670  nrejected = 0;
1671  cpl_size i, im = 0;
1672  for (i = 0; i < n; i++) {
1673  if (vv[i] < vlo || vv[i] > vhi) { /* skip this entry */
1674  nrejected++;
1675  continue;
1676  }
1677  if (ve) {
1678  value += vv[i] / ve[i];
1679  weight += 1. / ve[i];
1680  } else {
1681  value += vv[i];
1682  weight += 1.;
1683  }
1684  if (vv[i] < min) {
1685  min = vv[i];
1686  }
1687  if (vv[i] > max) {
1688  max = vv[i];
1689  }
1690  if (aMedian) {
1691  cpl_vector_set(vmedian, im++, vv[i]);
1692  }
1693  } /* for i (slice table rows) */
1694  value /= weight;
1695  double wmse = 0.; /* compute weighted MSE */
1696  for (i = 0; i < n; i++) {
1697  if (vv[i] < vlo || vv[i] > vhi) {
1698  continue;
1699  }
1700  if (ve) {
1701  wmse += pow(vv[i] - value, 2) / ve[i];
1702  } else {
1703  wmse += pow(vv[i] - value, 2);
1704  }
1705 #if 0
1706  if (aMedian) {
1707  cpl_msg_debug("vpos", "%f %f %f -> %f", vv[i], vv[i] - value,
1708  pow(vv[i] - value, 2), wmse);
1709  }
1710 #endif
1711  } /* for i (slice table rows) */
1712  wmse /= weight;
1713  if (aMedian) {
1714  cpl_vector_set_size(vmedian, im); /* delete unused entries */
1715  *aMedian = cpl_vector_get_median(vmedian);
1716  cpl_vector_delete(vmedian);
1717  }
1718  /* compute sigma (if we have a valid result) */
1719  if (isnormal(weight) && isfinite(wmse)) {
1720  /* full propagated errors: direct variances plus the weighted MSE */
1721  /* XXX where did this formula originally come from?!? */
1722  sigma = sqrt(ve ? (n - nrejected) / (weight*weight) : 0. + wmse);
1723 #if 0
1724  if (aMedian) {
1725  cpl_msg_debug("vpos", "%f", sigma);
1726  }
1727 #endif
1728  } /* if */
1729  /* compute the new limits (if we weighted any points) */
1730  if (isfinite(value)) {
1731  vlo = value - aSigma * sigma;
1732  vhi = value + aSigma * sigma;
1733  } /* if */
1734 #if 0
1735  cpl_msg_debug(func, "%s/%s: %f +/- %f (+/- %f) --> %f...%f (rej. %"
1736  CPL_SIZE_FORMAT" / %"CPL_SIZE_FORMAT")", aCol, aColErr,
1737  value, sqrt(wmse), sigma, vlo, vhi, nrejected, n);
1738 #endif
1739  } while (nrejected < n && (max > vhi || min < vlo));
1740 
1741  /* last resort, in case all points got rejected: re-use the *
1742  * previous ranges (they were then not overwritten above) and *
1743  * use those as central/final value and their sigma */
1744  if (nrejected == n) {
1745  value = (vlo + vhi) / 2.;
1746  sigma = (vhi - vlo) / (2. * aSigma);
1747 #if 0
1748  cpl_msg_debug(func, "%s/%s: %f +/- %f (ALL rej. %"CPL_SIZE_FORMAT" / %"
1749  CPL_SIZE_FORMAT")", aCol, aColErr, value, sigma, nrejected, n);
1750 #endif
1751  }
1752 
1753  *aValue = value;
1754  if (aError) {
1755  *aError = sigma;
1756  }
1757  return nrejected;
1758 } /* muse_geo_determine_horizontal_wmean() */
1759 
1760 /*----------------------------------------------------------------------------*/
1781 /*----------------------------------------------------------------------------*/
1782 static void
1783 muse_geo_determine_horizontal_vpos(const cpl_table *aSlice,
1784  double *aP, double *aPE, double *aPM,
1785  double aDY, double aF)
1786 {
1787  const char func[] = "muse_geo_determine_horizontal"; /* pretend to be there */
1788  if (!aSlice) return;
1789  if (!aP || !aPE || !aPM) return;
1790 
1791  cpl_vector *vvpos = cpl_vector_wrap(cpl_table_get_nrow(aSlice),
1792  (double *)cpl_table_get_data_double_const(aSlice,
1793  "vpos"));
1794  *aP = cpl_vector_get_mean(vvpos);
1795  *aPE = cpl_vector_get_stdev(vvpos);
1796  *aPM = cpl_vector_get_median_const(vvpos);
1797  /* if the spread is large, then there might be spots *
1798  * from different pinholes involved! */
1799  if (*aPE < 0.01) { /* this should already be good enough */
1800  cpl_vector_unwrap(vvpos);
1801  return;
1802  } /* if */
1803 
1804  cpl_size n = cpl_vector_get_size(vvpos);
1805  cpl_vector *vpp = cpl_vector_duplicate(vvpos),
1806  *vres = cpl_vector_duplicate(vvpos),
1807  *vmedian = cpl_vector_new(n);
1808  cpl_vector_unwrap(vvpos);
1809  cpl_vector_fill(vmedian, *aPM);
1810  cpl_vector_subtract(vres, vmedian);
1811  cpl_vector_delete(vmedian);
1812  double min = cpl_vector_get_min(vres),
1813  max = cpl_vector_get_max(vres);
1814  cpl_msg_debug(func, "vector of vpos values (%.4f +/- %.4f, %.4f) and "
1815  "residuals (%.4f ... %.4f), pinhole distance %.4f",
1816  *aP, *aPE, *aPM, min, max, aDY * aF);
1817 #if 0
1818  cpl_bivector *biv = cpl_bivector_wrap_vectors(vpp, vres);
1819  cpl_bivector_dump(biv, stdout);
1820  fflush(stdout);
1821  cpl_bivector_unwrap_vectors(biv);
1822 #endif
1823  /* Also check the absolute residuals. This case is useful, when *
1824  * half of the value happen to be above and half below the mean, *
1825  * by approximately one pinhole distance -> halfandhalf = true! */
1826  cpl_array *ares = cpl_array_wrap_double(cpl_vector_unwrap(vres), n);
1827  cpl_array_abs(ares);
1828  double amin = cpl_array_get_min(ares),
1829  amax = cpl_array_get_max(ares);
1830  cpl_boolean halfandhalf = fabs(aDY/2. - amin) < 0.01
1831  && fabs(aDY/2. - amax) < 0.01;
1832  cpl_array_delete(ares);
1833 
1834  double p2, pe2, pm2;
1835  cpl_size i;
1836  double limit = fabs(0.9 * aDY * aF);
1837  if (!halfandhalf && /* halfandhalf should be rectified by the else case */
1838  fabs(min) < limit && fabs(max) < limit) {
1839  /* If the largest residuals are less than 90% of the pinhole distance *
1840  * then shifting by one pinhole will not help. Then just compute mean *
1841  * and error, clipping the outliers as for the other properties. */
1842  cpl_size nrej = muse_geo_determine_horizontal_wmean(aSlice, "vpos", NULL,
1843  &p2, &pe2, &pm2, 2.);
1844  cpl_msg_debug(func, "values after rejection vector of vpos values (%.4f "
1845  "+/- %.4f, %.4f), %"CPL_SIZE_FORMAT" rejected", p2, pe2, pm2,
1846  nrej);
1847  } else { /* Largest residual is of the order of the pinhole distance. *
1848  * Try to fix that, by looping through the vector and subtract *
1849  * the pinhole distance from the high values. */
1850  cpl_size nfixed = 0;
1851  for (i = 0; i < n; i++) {
1852  double v = cpl_vector_get(vpp, i);
1853  if (v > *aP) { /* subtract from all values greater than the mean */
1854  cpl_vector_set(vpp, i, v - aDY * aF);
1855  nfixed++;
1856  } /* if */
1857  } /* for i */
1858  p2 = cpl_vector_get_mean(vpp);
1859  pe2 = cpl_vector_get_stdev(vpp);
1860  pm2 = cpl_vector_get_median_const(vpp);
1861  cpl_msg_debug(func, "fixed vector of vpos values (%.4f +/- %.4f, %.4f), "
1862  "%"CPL_SIZE_FORMAT" fixed", p2, pe2, pm2, nfixed);
1863 #if 0
1864  cpl_vector_dump(vpp, stdout);
1865  fflush(stdout);
1866 #endif
1867  } /* else */
1868  cpl_vector_delete(vpp);
1869  if (pe2 < *aPE) { /* if improved, keep these values */
1870  *aP = p2;
1871  *aPE = pe2;
1872  *aPM = pm2;
1873  }
1874 } /* muse_geo_determine_horizontal_vpos() */
1875 
1876 /*----------------------------------------------------------------------------*/
1926 /*----------------------------------------------------------------------------*/
1929 {
1930  cpl_ensure(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT, NULL);
1931  cpl_size nrow = cpl_table_get_nrow(aGeo->table);
1932  cpl_ensure(nrow >= 50, CPL_ERROR_ILLEGAL_INPUT, NULL);
1933  cpl_ensure(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
1934  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
1935  const unsigned char ifu = cpl_table_get_column_min(aGeo->table, MUSE_GEOTABLE_FIELD),
1936  ifu2 = cpl_table_get_column_max(aGeo->table, MUSE_GEOTABLE_FIELD);
1937  cpl_ensure(ifu == ifu2 && ifu >= 1 && ifu <= kMuseNumIFUs,
1938  CPL_ERROR_ILLEGAL_INPUT, NULL);
1939 
1940  double maskangle = 0., fmaskrot = 1.;
1941  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
1942  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
1943  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
1944  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
1945  "(cos = %.4e)", maskangle, fmaskrot);
1946  }
1947  double pinholedy = kMuseCUmpmDY;
1948  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
1949  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
1950  cpl_msg_warning(__func__, "Using pinhole y distance of %f mm (instead of "
1951  "%f mm)", pinholedy, kMuseCUmpmDY);
1952  }
1953  cpl_boolean stdgap = getenv("MUSE_GEOMETRY_STD_GAP")
1954  && atoi(getenv("MUSE_GEOMETRY_STD_GAP")) > 0;
1955  if (stdgap) {
1956  cpl_msg_warning(__func__, "Using old (standard) gap computation");
1957  } else {
1958  cpl_msg_info(__func__, "Using new (alternative) gap computation");
1959  }
1960  const double kScaleX = kMuseTypicalCubeSizeX * aGeo->scale / 60.;
1961 
1962  /* select the middle spot, to get per-slice properties */
1964  cpl_table_and_selected_int(gt->table, "spot", CPL_NOT_EQUAL_TO, 2);
1965  cpl_table_erase_selected(gt->table);
1966  /* now results for different wavelengths are adjacent */
1967 #if 0
1968  FILE *f = fopen("bla_horizontal.dat", "w");
1969  fprintf(f, "#");
1970  cpl_table_dump(gt->table, 0, 100000000, f);
1971  fclose(f);
1972  f = fopen("bla_lambdas.dat", "w");
1973  fprintf(f, "#");
1974  cpl_table_dump(aGeo->table, 0, 100000000, f);
1975  fclose(f);
1976 #endif
1977  unsigned short nslice;
1978  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
1979  /* get the entries for this one slice */
1980  cpl_table_select_all(gt->table);
1981  cpl_table_and_selected_int(gt->table, "SliceCCD", CPL_EQUAL_TO, nslice);
1982  if (cpl_table_count_selected(gt->table) < 1) {
1983  continue;
1984  }
1985  cpl_array *asel = cpl_table_where_selected(gt->table);
1986  cpl_table *slice = cpl_table_extract_selected(gt->table);
1987 #if 0
1988  cpl_table_dump(slice, 0, 1000, stdout);
1989  fflush(stdout);
1990 #endif
1991 
1992  /* compute weighted averages for the geometrical properties */
1993  double a, ae, w, we, xr, xre, dxl, dxr;
1994  muse_geo_determine_horizontal_wmean(slice, MUSE_GEOTABLE_ANGLE,
1995  MUSE_GEOTABLE_ANGLE"err", &a, &ae,
1996  NULL, 2.);
1997  muse_geo_determine_horizontal_wmean(slice, MUSE_GEOTABLE_WIDTH,
1998  MUSE_GEOTABLE_WIDTH"err", &w, &we,
1999  NULL, 1.5);
2000  muse_geo_determine_horizontal_wmean(slice, "xrel", "xrelerr", &xr, &xre,
2001  NULL, 1.5);
2002  muse_geo_determine_horizontal_wmean(slice, "dxl", NULL, &dxl, NULL, NULL, 2.);
2003  muse_geo_determine_horizontal_wmean(slice, "dxr", NULL, &dxr, NULL, NULL, 2.);
2004 
2005  /* compute the average +/- stdev of the "vpos" values, too */
2006  cpl_errorstate ps = cpl_errorstate_get();
2007  double p = NAN, pe = -1, pm = NAN;
2008  muse_geo_determine_horizontal_vpos(slice, &p, &pe, &pm,
2009  pinholedy, fmaskrot);
2010  if (pe < 0 && !cpl_errorstate_is_equal(ps)) { /* some error, possibly only a single value */
2011  pe = 0.1; /* better something large than something negative... */
2012  cpl_errorstate_set(ps);
2013  }
2014 
2015  cpl_msg_debug(__func__, "IFU %2hhu stack %1d slice %2d / %2d "
2016  "angle %6.3f +/- %.3f deg width %.3f +/- %.3f pix "
2017  "xrel %.4f +/- %.4f vpos %.4f +/- %.4f (%.4f)", ifu,
2018  cpl_table_get_int(slice, "stack", 0, NULL), nslice,
2019  cpl_table_get_int(slice, "SliceSky", 0, NULL), a, ae, w, we,
2020  xr, xre, p, pe, pm);
2021  /* replace first selected line with the compute properties, *
2022  * unselect it then erase the other selected ones */
2023  cpl_size irow = cpl_array_get_cplsize(asel, 0, NULL);
2024  cpl_array_delete(asel);
2025  cpl_table_set_double(gt->table, MUSE_GEOTABLE_ANGLE, irow, a);
2026  cpl_table_set_double(gt->table, MUSE_GEOTABLE_ANGLE"err", irow, ae);
2027  cpl_table_set_double(gt->table, MUSE_GEOTABLE_WIDTH, irow, w);
2028  cpl_table_set_double(gt->table, MUSE_GEOTABLE_WIDTH"err", irow, we);
2029  cpl_table_set_double(gt->table, "xrel", irow, xr);
2030  cpl_table_set_double(gt->table, "xrelerr", irow, xre);
2031  cpl_table_set_double(gt->table, "vpos", irow, p);
2032  cpl_table_set_double(gt->table, "vposerr", irow, pe);
2033  cpl_table_set_double(gt->table, "dxl", irow, dxl);
2034  cpl_table_set_double(gt->table, "dxr", irow, dxr);
2035  /* invalidate some columns that do not make sense any more, do not erase *
2036  * them yet, so that other tables of the same basic format can be appended */
2037  cpl_table_set_invalid(gt->table, "flux", irow);
2038  cpl_table_set_invalid(gt->table, "lambda", irow);
2039  cpl_table_set_invalid(gt->table, "xc", irow);
2040  cpl_table_set_invalid(gt->table, "yc", irow);
2041  cpl_table_unselect_row(gt->table, irow);
2042  cpl_table_erase_selected(gt->table);
2043  cpl_table_delete(slice);
2044  } /* for nslice (all slices) */
2045 #if 0
2046  printf("intermediate result: weighted averages of angle, width, and xrel:\n");
2047  cpl_table_dump(gt->table, 0, 1000000, stdout);
2048  fflush(stdout);
2049 #endif
2050 
2051  /* Now go through one slicer stack, and for each slice with a result find *
2052  * the other three (horizontally adjacent) slices. Compute the gaps between *
2053  * the slices and add them to the total budget to derive the central *
2054  * horizontal position of all four slices in this row of the slicer stack. */
2055  /* XXX how does the angle (of the mask, each IFU, or *
2056  * the individual slices) affect the result? */
2057  const unsigned short nsoff = kMuseSlicesPerCCD / 4; /* slice offset */
2058  for (nslice = 1 + nsoff; nslice <= 2*nsoff; nslice++) {
2059  /* get the entries for this one slice by sky numbering */
2060  cpl_table_unselect_all(gt->table);
2061  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice - nsoff);
2062  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice);
2063  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice + nsoff);
2064  cpl_table_or_selected_int(gt->table, "SliceSky", CPL_EQUAL_TO, nslice + 2*nsoff);
2065  /* extract the selected rows so that we can access them easily */
2066  cpl_table *ts = cpl_table_extract_selected(gt->table);
2067  /* get the row indices for all four slicer stacks (if possible) */
2068  int irow, nrowts = cpl_table_get_nrow(ts),
2069  i1 = -1, i2 = -1, i3 = -1, i4 = -1;
2070  for (irow = 0; irow < nrowts; irow++) {
2071  /* here, we index the stacks again in the same way as the *
2072  * optical numbering, from right to left in the field of view */
2073  switch (cpl_table_get_int(ts, "stack", irow, NULL)) {
2074  case 1: i1 = irow; break;
2075  case 2: i2 = irow; break;
2076  case 3: i3 = irow; break;
2077  case 4: i4 = irow; break;
2078  } /* switch */
2079  } /* for irow */
2080  /* at least the two central slicer stacks are required */
2081  if (i3 < 0 || i2 < 0) {
2082  char *msg = cpl_sprintf("For IFU %2hhu / row %2d in the slicer stacks "
2083  "(slice sky numbers %02d, %02d, %02d, %02d), at "
2084  "least one of the two middle stacks (%s/%s) is "
2085  "missing", ifu, nslice - nsoff,
2086  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff,
2087  i3 < 0 ? "left" : "-", i2 < 0 ? "right" : "-");
2088  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "%s", msg);
2089  cpl_msg_error(__func__, "%s", msg);
2090  cpl_free(msg);
2091  cpl_table_dump(ts, 0, 10000, stdout);
2092  cpl_table_delete(ts);
2093  continue;
2094  }
2095 
2096  double *xrel = cpl_table_get_data_double(ts, "xrel"),
2097  *xrerr = cpl_table_get_data_double(ts, "xrelerr"),
2098  *width = cpl_table_get_data_double(ts, MUSE_GEOTABLE_WIDTH),
2099  *werr = cpl_table_get_data_double(ts, MUSE_GEOTABLE_WIDTH"err"),
2100  *pdxl = cpl_table_get_data_double(ts, "dxl"),
2101  *pdxr = cpl_table_get_data_double(ts, "dxr");
2102  /* also need the slice width in mm here, so create a new temporary column */
2103  cpl_table_duplicate_column(ts, "width_mm",
2104  ts, MUSE_GEOTABLE_WIDTH);
2105  cpl_table_multiply_scalar(ts, "width_mm", 1. / kScaleX);
2106  cpl_table_set_column_unit(ts, "width_mm", "mm");
2107  cpl_table_duplicate_column(ts, "widtherr_mm",
2108  ts, MUSE_GEOTABLE_WIDTH"err");
2109  cpl_table_multiply_scalar(ts, "widtherr_mm", 1. / kScaleX);
2110  cpl_table_set_column_unit(ts, "widtherr_mm", "mm");
2111  double *wmm = cpl_table_get_data_double(ts, "width_mm"),
2112  *werrmm = cpl_table_get_data_double(ts, "widtherr_mm");
2113 
2114  double cgap1 = (3. * kMuseCUmpmDX / fmaskrot
2115  - (xrel[i3] + wmm[i3] / 2. + wmm[i2] / 2. - xrel[i2])) /* [mm] */
2116  * kScaleX,
2117  cgerr = sqrt(pow(xrerr[i3], 2) + pow(xrerr[i2], 2) +
2118  pow(werrmm[i3] / 2., 2) + pow(werrmm[i2] / 2., 2))
2119  * kScaleX,
2120  cgap2 = kMuseCUmpmDX * (1. - pdxl[i3] - pdxr[i2]) /* [mm] */
2121  * kScaleX,
2122  cgap = stdgap ? cgap1 : cgap2;
2123 #if 0
2124  cpl_msg_debug(__func__, "cgap: %f, %f +/- %f", cgap1, cgap2, cgerr);
2125 #endif
2126  /* for unlikely size, set error and output error message (the *
2127  * error state might otherwise be swallowed by parallelization!) */
2128  if (cgap < 0 || cgap > 0.5) {
2129  cpl_msg_warning(__func__, "For IFU %2hhu / row %2d in the slicer stacks "
2130  "(slice sky numbers %02d, %02d, %02d, %02d), the central "
2131  "gap is unlikely (%f), reset to %.2f pix", ifu,
2132  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2133  nslice + 2*nsoff, cgap, kMuseGeoMiddleGap);
2134  cgerr += sqrt(fabs(cgap)); /* add to the error estimate */
2135  cgap = kMuseGeoMiddleGap;
2136  }
2137  /* use this to compute effective centers of the two middle *
2138  * slices, distributing the gap equally to both sides */
2139  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i3, -(cgap / 2. + width[i3] / 2.));
2140  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i2, cgap / 2. + width[i2] / 2.);
2141  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i3,
2142  sqrt(cgerr*cgerr + werr[i3]*werr[i3]) / 2.);
2143  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i2,
2144  sqrt(cgerr*cgerr + werr[i2]*werr[i2]) / 2.);
2145 
2146  double lgap = NAN, lgerr = NAN;
2147  if (i4 >= 0) {
2148  /* now compute the left gap, using the central *
2149  * gap and the widths of the two left slices */
2150  lgerr = sqrt(pow(xrerr[i4], 2) + pow(xrerr[i3], 2) +
2151  pow(werrmm[i4] / 2., 2) + pow(werrmm[i3] / 2., 2))
2152  * kScaleX;
2153  double lgap1 = (3. * kMuseCUmpmDX / fmaskrot
2154  - (xrel[i4] + wmm[i4] / 2. + wmm[i3] / 2. - xrel[i3]))
2155  * kScaleX,
2156  lgap2 = kMuseCUmpmDX * (1. - pdxl[i4] - pdxr[i3]) /* [mm] */
2157  * kScaleX;
2158  lgap = stdgap ? lgap1 : lgap2;
2159 #if 0
2160  cpl_msg_debug(__func__, "lgap: %f, %f +/- %f", lgap1, lgap2, lgerr);
2161 #endif
2162  if (lgap < 0 || lgap > 0.5) {
2163  cpl_msg_warning(__func__, "For IFU %2hhu / row %2d in the slicer stacks"
2164  " (slice sky numbers %02d, %02d, %02d, %02d), the left "
2165  "gap is unlikely (%f), reset to %.2f pix", ifu,
2166  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2167  nslice + 2*nsoff, lgap, kMuseGeoOuterGap);
2168  lgerr += sqrt(fabs(lgap)); /* add to the error estimate */
2169  lgap = kMuseGeoOuterGap;
2170  }
2171  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i4,
2172  -(cgap / 2. + width[i3] + lgap + width[i4] / 2.));
2173  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i4,
2174  sqrt(cgerr*cgerr / 4. + werr[i3]*werr[i3]
2175  + lgerr*lgerr + werr[i4]*werr[i4] / 4.));
2176  } else {
2177  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "For IFU %2hhu /"
2178  " row %2d in the slicer stacks (slice sky numbers"
2179  " %02d, %02d, %02d, %02d), the leftmost stack is "
2180  "missing", ifu, nslice - nsoff,
2181  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff);
2182  }
2183 
2184  double rgap = NAN, rgerr = NAN;
2185  if (i1 >= 0) {
2186  /* and finally the right gap and the position of the rightmost slice */
2187  rgerr = sqrt(pow(xrerr[i2], 2) + pow(xrerr[i1], 2) +
2188  pow(werrmm[i2] / 2., 2) + pow(werrmm[i1] / 2., 2))
2189  * kScaleX;
2190  double rgap1 = (3. * kMuseCUmpmDX / fmaskrot
2191  - (xrel[i2] + wmm[i2] / 2. + wmm[i1] / 2. - xrel[i1]))
2192  * kScaleX,
2193  rgap2 = kMuseCUmpmDX * (1. - pdxl[i2] - pdxr[i1]) /* [mm] */
2194  * kScaleX;
2195  rgap = stdgap ? rgap1 : rgap2;
2196 #if 0
2197  cpl_msg_debug(__func__, "rgap: %f, %f +/- %f", rgap1, rgap2, rgerr);
2198 #endif
2199  if (rgap < 0 || rgap > 0.5) {
2200  cpl_msg_warning(__func__, "For IFU %2hhu / row %2d in the slicer stacks"
2201  " (slice sky numbers %02d, %02d, %02d, %02d), the right"
2202  " gap is unlikely (%f), reset to %.2f pix", ifu,
2203  nslice - nsoff, nslice - nsoff, nslice, nslice + nsoff,
2204  nslice + 2*nsoff, rgap, kMuseGeoOuterGap);
2205  rgerr += sqrt(fabs(rgap)); /* add to the error estimate */
2206  rgap = kMuseGeoOuterGap;
2207  }
2208  cpl_table_set_double(ts, MUSE_GEOTABLE_X, i1,
2209  cgap / 2. + width[i2] + rgap + width[i1] / 2.);
2210  cpl_table_set_double(ts, MUSE_GEOTABLE_X"err", i1,
2211  sqrt(cgerr*cgerr / 4. + werr[i2]*werr[i2]
2212  + rgerr*rgerr + werr[i1]*werr[i1] / 4.));
2213  } else {
2214  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "For IFU %2hhu /"
2215  " row %2d in the slicer stacks (slice sky numbers"
2216  " %02d, %02d, %02d, %02d), the rightmost stack is"
2217  " missing", ifu, nslice - nsoff,
2218  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff);
2219  }
2220  cpl_msg_debug(__func__, "IFU %2hhu row %2d gaps (slice sky numbers %02d, "
2221  "%02d, %02d, %02d): central %.3f +/- %.3f pix, left %.3f +/- "
2222  "%.3f pix, right %.3f +/- %.3f pix", ifu, nslice - nsoff,
2223  nslice - nsoff, nslice, nslice + nsoff, nslice + 2*nsoff,
2224  cgap, cgerr, lgap, lgerr, rgap, rgerr);
2225 
2226  /* remove the temporary column again... */
2227  cpl_table_erase_column(ts, "width_mm");
2228  cpl_table_erase_column(ts, "widtherr_mm");
2229 
2230  /* erase the original entries in the gt->table, and replace them *
2231  * with the ones we modified here, which contain the center positions */
2232  cpl_table_erase_selected(gt->table);
2233  cpl_table_insert(gt->table, ts, cpl_table_get_nrow(gt->table));
2234  cpl_table_delete(ts);
2235  } /* for nslice (all slices in the middle left stack) */
2236 
2237  /* sort gt->table again, by decreasing stack and increasing sky number */
2238  cpl_propertylist *order = cpl_propertylist_new();
2239  cpl_propertylist_append_bool(order, "stack", CPL_TRUE);
2240  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2241  cpl_table_sort(gt->table, order);
2242  cpl_propertylist_delete(order);
2243 #if 0
2244  printf("intermediate result: only the y position is still missing:\n");
2245  cpl_table_dump(gt->table, 0, 1000000, stdout);
2246  fflush(stdout);
2247 #endif
2248  return gt;
2249 } /* muse_geo_determine_horizontal() */
2250 
2251 /*----------------------------------------------------------------------------*/
2265 /*----------------------------------------------------------------------------*/
2266 static unsigned char
2267 muse_geo_select_reference(const muse_geo_table *aGeo, unsigned short *aSlice)
2268 {
2269  unsigned char ifu = 0;
2270  unsigned short slice = 0;
2271  /* copy table to not touch the original */
2272  cpl_table *geo = cpl_table_duplicate(aGeo->table);
2273  cpl_table_unselect_all(geo);
2274  cpl_table_or_selected_int(geo, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, 12);
2275  cpl_table_and_selected_int(geo, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2276  int nsel = cpl_table_count_selected(geo);
2277  if (nsel > 0) {
2278  ifu = 12;
2279  slice = 24;
2280  }
2281  unsigned char testifu = 13,
2282  testslice = 18;
2283  short testoffset = 1;
2284  while (ifu == 0 && slice == 0) {
2285  cpl_table_unselect_all(geo);
2286  cpl_table_or_selected_int(geo, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, testifu);
2287  cpl_table_and_selected_int(geo, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, testslice);
2288  nsel = cpl_table_count_selected(geo);
2289  if (nsel > 0) {
2290  ifu = testifu;
2291  slice = testslice;
2292  }
2293  testifu += testoffset;
2294  if (testifu > kMuseNumIFUs) { /* reached the end, start again from the middle */
2295  testifu = 11;
2296  testoffset = -1;
2297  }
2298  } /* while */
2299  cpl_table_delete(geo);
2300  if (aSlice) {
2301  *aSlice = slice;
2302  }
2303  return ifu;
2304 } /* muse_geo_select_reference() */
2305 
2306 /*----------------------------------------------------------------------------*/
2357 /*----------------------------------------------------------------------------*/
2358 cpl_error_code
2360 {
2361  cpl_ensure_code(aGeo && aGeo->table && aSpots, CPL_ERROR_NULL_INPUT);
2362  cpl_size nrow = cpl_table_get_nrow(aGeo->table);
2363  cpl_ensure_code(nrow >= 50, CPL_ERROR_ILLEGAL_INPUT);
2364  cpl_ensure_code(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
2365  CPL_ERROR_INCOMPATIBLE_INPUT);
2366  cpl_ensure_code(muse_cpltable_check(aSpots, muse_geo_measurements_def) == CPL_ERROR_NONE,
2367  CPL_ERROR_INCOMPATIBLE_INPUT);
2368  const unsigned char ifu1 = cpl_table_get_column_min(aGeo->table, MUSE_GEOTABLE_FIELD),
2369  ifu2 = cpl_table_get_column_max(aGeo->table, MUSE_GEOTABLE_FIELD);
2370  if (!ifu1 || !ifu2 || ifu1 == ifu2) {
2371  return cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
2372  "input geometry table contains data of IFUs "
2373  "%2hhu .. %2hhu", ifu1, ifu2);
2374  }
2375  cpl_ensure_code(cpl_table_get_column_stdev(aSpots, "ScaleFOV") < 1e-10,
2376  CPL_ERROR_ILLEGAL_INPUT);
2377  cpl_array *hoffsets = NULL;
2378  if (getenv("MUSE_GEOMETRY_HORI_OFFSETS")) {
2380  getenv("MUSE_GEOMETRY_HORI_OFFSETS"), ",");
2381  cpl_msg_warning(__func__, "Overriding horizontal offsets, found %"
2382  CPL_SIZE_FORMAT" values!", cpl_array_get_size(hoffsets));
2383  }
2384  const double kScaleX = kMuseTypicalCubeSizeX * aGeo->scale / 60.;
2385 
2386  /* create and fill the SliceSky and stack table columns in the spots table */
2387  cpl_table_new_column(aSpots, MUSE_GEOTABLE_SKY, CPL_TYPE_INT);
2388  cpl_table_new_column(aSpots, "stack", CPL_TYPE_INT);
2389  cpl_size ispot, nspots = cpl_table_get_nrow(aSpots);
2390  for (ispot = 0; ispot < nspots; ispot++) {
2391  int nslice = cpl_table_get_int(aSpots, "SliceCCD", ispot, NULL);
2392  cpl_table_set(aSpots, MUSE_GEOTABLE_SKY, ispot, kMuseGeoSliceSky[nslice - 1]);
2393  unsigned char stack = nslice <= 12 ? 4 : (nslice <= 24 ? 3 : (nslice <= 36 ? 2 : 1));
2394  cpl_table_set_int(aSpots, "stack", ispot, stack);
2395  } /* for ispot */
2396 
2397  /* extract top and bottom rows of the middle slicer stack *
2398  * to make the table smaller and better to handle */
2399  const unsigned short nsoff = kMuseSlicesPerCCD / 4; /* slice offset */
2400  cpl_table_unselect_all(aSpots);
2401  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 - nsoff + 1);
2402  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2403  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + 1);
2404  cpl_table_or_selected_int(aSpots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + nsoff);
2405 #if 0
2406  cpl_msg_debug(__func__, "All top/bottom spots selected: %"
2407  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2408 #endif
2409  /* also apply a lower flux limit of 500. */
2410  cpl_table_and_selected_double(aSpots, "flux", CPL_NOT_LESS_THAN, 500.);
2411 #if 0
2412  cpl_msg_debug(__func__, "All bright top/bottom spots selected: %"
2413  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2414 #endif
2415  /* only select the middle spot in each slice */
2416  cpl_table_and_selected_int(aSpots, "SpotNo", CPL_EQUAL_TO, 2);
2417 #if 0
2418  cpl_msg_debug(__func__, "All bright and central top/bottom spots selected: %"
2419  CPL_SIZE_FORMAT, cpl_table_count_selected(aSpots));
2420 #endif
2421  cpl_table *tbspots = cpl_table_extract_selected(aSpots); /* top/bottom spots */
2422  cpl_msg_debug(__func__, "All spots: %"CPL_SIZE_FORMAT", top/bottom spots to "
2423  "work with: %"CPL_SIZE_FORMAT, nspots, cpl_table_get_nrow(tbspots));
2424  nspots = cpl_table_get_nrow(tbspots);
2425 
2426  /* variables for the corrective loops */
2427  int *ifus = cpl_table_get_data_int(aGeo->table, MUSE_GEOTABLE_FIELD),
2428  *slices = cpl_table_get_data_int(aGeo->table, MUSE_GEOTABLE_SKY);
2429  double *xpos = cpl_table_get_data_double(aGeo->table, MUSE_GEOTABLE_X),
2430  *xerr = cpl_table_get_data_double(aGeo->table, MUSE_GEOTABLE_X"err"),
2431  *xrel = cpl_table_get_data_double(aGeo->table, "xrel");
2432  int irow;
2433 
2434  /* loop over the IFUs */
2435  unsigned char nifu;
2436  for (nifu = ifu1; nifu < ifu2; nifu++) { /* exclude the last IFU! */
2437  /* select and extract bottom slices of current IFU */
2438  cpl_table_unselect_all(tbspots);
2439  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24);
2440  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + nsoff);
2441  cpl_table_and_selected_int(tbspots, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
2442  cpl_table *xspots = cpl_table_extract_selected(tbspots);
2443  /* select and extract top slices of next IFU (below) */
2444  cpl_table_unselect_all(tbspots);
2445  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 - nsoff + 1);
2446  cpl_table_or_selected_int(tbspots, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 24 + 1);
2447  cpl_table_and_selected_int(tbspots, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu + 1);
2448  cpl_table *tmp = cpl_table_extract_selected(tbspots);
2449  cpl_table_insert(xspots, tmp, cpl_table_get_nrow(xspots));
2450  cpl_table_delete(tmp);
2451  int nxspots = cpl_table_get_nrow(xspots);
2452 
2453  /* sort the table, mainly to get ordered output for debugging */
2454  cpl_propertylist *order = cpl_propertylist_new();
2455  cpl_propertylist_append_bool(order, "lambda", CPL_FALSE);
2456  cpl_propertylist_append_bool(order, "VPOS", CPL_FALSE);
2457  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2458  cpl_table_sort(xspots, order);
2459  cpl_propertylist_delete(order);
2460 #if 0
2461  printf("ifu %2hhu:\n", nifu);
2462  cpl_table_dump(xspots, 0, nxspots, stdout);
2463  fflush(stdout);
2464 #endif
2465 
2466  /* create array for the dxcen-statistics and respective index */
2467  cpl_array *apos2 = cpl_array_new(nxspots, CPL_TYPE_DOUBLE),
2468  *apos3 = cpl_array_new(nxspots, CPL_TYPE_DOUBLE);
2469  int idx2 = 0, idx3 = 0;
2470 
2471  /* loop over all wavelengths, and positions to find common spots in adjacent IFUs */
2472  cpl_vector *vtmp = cpl_vector_wrap(nxspots,
2473  cpl_table_get_data_double(xspots, "lambda")),
2474  *lambdas = muse_cplvector_get_unique(vtmp);
2475  cpl_vector_unwrap(vtmp);
2476  vtmp = cpl_vector_wrap(nxspots, cpl_table_get_data_double(xspots, "VPOS"));
2477  cpl_vector *positions = muse_cplvector_get_unique(vtmp);
2478  cpl_vector_unwrap(vtmp);
2479  int ilambda, nlambda = cpl_vector_get_size(lambdas);
2480  for (ilambda = 0; ilambda < nlambda; ilambda++) {
2481  double lambda = cpl_vector_get(lambdas, ilambda);
2482 
2483  int ipos, npos = cpl_vector_get_size(positions);
2484  for (ipos = 0; ipos < npos; ipos++) {
2485  double vpos = cpl_vector_get(positions, ipos);
2486 
2487  int nstack; /* two middle stacks, from left to right in the FOV */
2488  for (nstack = 3; nstack >= 2; nstack--) {
2489  cpl_table_select_all(xspots);
2490  cpl_table_and_selected_double(xspots, "lambda", CPL_EQUAL_TO, lambda);
2491  cpl_table_and_selected_double(xspots, "VPOS", CPL_EQUAL_TO, vpos);
2492  cpl_table_and_selected_int(xspots, "stack", CPL_EQUAL_TO, nstack);
2493  int nsel = cpl_table_count_selected(xspots);
2494  if (nsel != 2) {
2495 #if 0
2496  printf("ifu %2hhu, lambda = %f, VPOS = %f, stack = %d: %d selected rows!\n",
2497  nifu, lambda, vpos, nstack, nsel);
2498 #endif
2499  continue;
2500  }
2501  /* extract and sort the table of common positions */
2502  cpl_table *common = cpl_table_extract_selected(xspots);
2503  order = cpl_propertylist_new();
2504  cpl_propertylist_append_bool(order, "SubField", CPL_FALSE);
2505  cpl_propertylist_append_bool(order, "SliceSky", CPL_FALSE);
2506  cpl_table_sort(common, order);
2507  cpl_propertylist_delete(order);
2508  int nselthis = cpl_table_and_selected_int(common, "SubField",
2509  CPL_EQUAL_TO, nifu);
2510  cpl_table_select_all(common);
2511  int nselnext = cpl_table_and_selected_int(common, "SubField",
2512  CPL_EQUAL_TO, nifu + 1);
2513  if (nselthis != 1 || nselnext != 1) {
2514 #if 0
2515  printf("\nifu %2hhu, lambda = %f, VPOS = %f, stack = %d: "
2516  "%d rows of IFU %2hhu, %d rows of IFU %2hhu!\n",
2517  nifu, lambda, vpos, nstack, nselthis, nifu, nselnext, nifu + 1);
2518 #endif
2519  //cpl_table_dump(common, 0, 100000, stdout);
2520  cpl_table_delete(common);
2521  continue;
2522  }
2523  /* now, this IFU is in row 0, the next IFU is in row 1, *
2524  * so we can finally compute the difference between both *
2525  * dxcen values, and store it in the array */
2526  double xdiff1 = cpl_table_get(common, "dxcen", 0, NULL),
2527  xdiff2 = cpl_table_get(common, "dxcen", 1, NULL),
2528  twidth1 = cpl_table_get(common, "twidth", 0, NULL),
2529  twidth2 = cpl_table_get(common, "twidth", 1, NULL);
2530  cpl_table_unselect_all(aGeo->table);
2531  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO,
2532  cpl_table_get_int(common, "SubField", 0, NULL));
2533  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO,
2534  cpl_table_get_int(common, "SliceSky", 0, NULL));
2535  cpl_array *sel = cpl_table_where_selected(aGeo->table);
2536  double width1 = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_WIDTH,
2537  cpl_array_get(sel, 0, NULL), NULL);
2538  cpl_array_delete(sel);
2539  cpl_table_unselect_all(aGeo->table);
2540  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO,
2541  cpl_table_get_int(common, "SubField", 1, NULL));
2542  cpl_table_or_selected_int(aGeo->table, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO,
2543  cpl_table_get_int(common, "SliceSky", 1, NULL));
2544  sel = cpl_table_where_selected(aGeo->table);
2545  double width2 = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_WIDTH,
2546  cpl_array_get(sel, 0, NULL), NULL);
2547  cpl_array_delete(sel);
2548 #if 0
2549  printf("\nifu %2hhu, lambda = %f, VPOS = %f, stack + %d, twidths: %f / %f, geowidths: %f / %f:\n",
2550  nifu, lambda, vpos, nstack, twidth1, twidth2, width1, width2);
2551  cpl_table_dump(common, 0, 100000, stdout);
2552  printf("==> xdiff = %f pix, %f pix (corrected)\n", xdiff1 - xdiff2,
2553  xdiff1 * width1 / twidth1 - xdiff2 * width2 / twidth2);
2554  fflush(stdout);
2555 #endif
2556  double xdiff = xdiff1 * width1 / twidth1 - xdiff2 * width2 / twidth2;
2557  if (nstack == 3) {
2558  cpl_array_set(apos3, idx3++, xdiff);
2559  }
2560  if (nstack == 2) {
2561  cpl_array_set(apos2, idx2++, xdiff);
2562  }
2563  cpl_table_delete(common);
2564  } /* for nstack */
2565  } /* for ipos */
2566  } /* for ilambda */
2569 #define HIST_BIN_WIDTH 0.1 /* 1/10 pix width of each histogram bin */
2570  cpl_bivector *hist2 = muse_cplarray_histogram(apos2, HIST_BIN_WIDTH, -5., 5.),
2571  *hist3 = muse_cplarray_histogram(apos3, HIST_BIN_WIDTH, -5., 5.);
2572  /* use a gap of 2 histogram entries below 0.5 to define a gap */
2573  muse_cplarray_erase_outliers(apos2, hist2, 2, 0.5);
2574  muse_cplarray_erase_outliers(apos3, hist3, 2, 0.5);
2575  cpl_bivector_delete(hist2);
2576  cpl_bivector_delete(hist3);
2577  cpl_array *apos = cpl_array_new(0, cpl_array_get_type(apos2));
2578  cpl_array_insert(apos, apos2, 0);
2579  cpl_array_insert(apos, apos3, cpl_array_get_size(apos));
2580 #if 0
2581  char *fn = cpl_sprintf("apos2_%02hhuto%02hhu.pos", nifu, nifu+1);
2582  FILE *fp = fopen(fn, "w");
2583  fprintf(fp, "# apos2 %2hhu to %2hhu:\n#", nifu, nifu+1);
2584  cpl_array_dump(apos2, 0, 1000000, fp);
2585  fclose(fp);
2586  cpl_free(fn);
2587  fn = cpl_sprintf("apos3_%02hhuto%02hhu.pos", nifu, nifu+1);
2588  fp = fopen(fn, "w");
2589  fprintf(fp, "# apos3 %2hhu to %2hhu:\n#", nifu, nifu+1);
2590  cpl_array_dump(apos3, 0, 1000000, fp);
2591  fclose(fp);
2592  cpl_free(fn);
2593  fn = cpl_sprintf("apos_%02hhuto%02hhu.pos", nifu, nifu+1);
2594  fp = fopen(fn, "w");
2595  fprintf(fp, "# apos %2hhu to %2hhu:\n#", nifu, nifu+1);
2596  cpl_array_dump(apos, 0, 1000000, fp);
2597  fclose(fp);
2598  cpl_free(fn);
2599 #endif
2600  /* compute both average positions, their sigmas, and use that to create *
2601  * a combined weighted average value with a corresponding error estimate */
2602  double mean2 = cpl_array_get_mean(apos2),
2603  stdev2 = cpl_array_get_stdev(apos2),
2604  var2 = stdev2 * stdev2,
2605  mean3 = cpl_array_get_mean(apos3),
2606  stdev3 = cpl_array_get_stdev(apos3),
2607  var3 = stdev3 * stdev3,
2608  mean = (mean2 + mean3) / 2.,
2609  wmean = (mean2 / var2 + mean3 / var3) / (1. / var2 + 1. / var3),
2610  wstdev = sqrt(1. / (1. / var2 + 1. / var3));
2611  cpl_array_delete(apos2);
2612  cpl_array_delete(apos3);
2613  cpl_msg_debug(__func__, "IFU %2hhu to IFU %2hhu: %6.3f +/- %5.3f pix "
2614  "[stack 3: %6.3f +/- %5.3f, stack2: %6.3f +/- %5.3f ==> %6.3f"
2615  " or %6.3f +/- %5.3f (%6.3f)]", nifu, nifu + 1, wmean, wstdev,
2616  mean3, stdev3, mean2, stdev2, mean,
2617  cpl_array_get_mean(apos), cpl_array_get_stdev(apos),
2618  cpl_array_get_median(apos));
2619  cpl_array_delete(apos);
2620  cpl_vector_delete(lambdas);
2621  cpl_vector_delete(positions);
2622  cpl_table_delete(xspots);
2623 
2624  /* now we have the offsets (and the error estimates), apply the shifts */
2625  for (irow = 0; irow < nrow; irow++) {
2626  if (ifus[irow] >= nifu + 1) {
2627  xpos[irow] -= wmean;
2628  xerr[irow] = sqrt(xerr[irow]*xerr[irow] + wstdev*wstdev);
2629  xrel[irow] += wmean / kScaleX;
2630  } /* if */
2631  } /* for irow (all table rows) */
2632  } /* for nifu */
2633  cpl_table_delete(tbspots);
2634 
2635  /* continue to see if there are manual adjustments to add */
2636  for (nifu = ifu1; nifu < ifu2; nifu++) { /* exclude the last IFU! */
2637  if (hoffsets) {
2638  double xdiff = 0.;
2639  if (cpl_array_get_size(hoffsets) >= nifu) { /* at least this many elements */
2640  const char *sdiff = cpl_array_get_string(hoffsets, nifu - 1);
2641  if (sdiff) {
2642  xdiff = atof(sdiff);
2643  } /* if override valid */
2644  } /* if override present from the next IFU */
2645  cpl_msg_debug(__func__, "Subtracting extra %7.4f pix from IFU %hhu onwards",
2646  xdiff, nifu + 1);
2647  for (irow = 0; irow < nrow; irow++) {
2648  if (ifus[irow] >= nifu + 1) {
2649  xpos[irow] -= xdiff;
2650  /* don't care about xrel in this case */
2651  } /* if */
2652  } /* for irow (all table rows) */
2653  continue; /* go to next IFU */
2654  } /* if hoffsets is there (override active) */
2655 #if 0
2656  printf("%s after correcting %hhu:\n", __func__, nifu + 1);
2657  cpl_table_dump(aGeo->table, 0, 1000000, stdout);
2658  fflush(stdout);
2659 #endif
2660  } /* for nifu */
2661  cpl_array_delete(hoffsets);
2662 
2663  /* find the horizontal center of the reference slice and shift the whole *
2664  * table so that the center is between the ref. slice and its neighbor */
2665  double xref = 0.;
2666  unsigned short sliceref;
2667  const unsigned char ifuref = muse_geo_select_reference(aGeo, &sliceref);
2668  for (irow = 0; irow < nrow; irow++) {
2669  if (ifus[irow] == ifuref &&
2670  (slices[irow] == sliceref || slices[irow] == sliceref + 12)) {
2671  xref += xpos[irow];
2672  } /* if */
2673  } /* for irow (all table rows) */
2674  xref /= 2.; /* current mean center */
2675  cpl_msg_debug(__func__, "Reference point (ifu %hhu, slicesky %hu/%hu) "
2676  "currently centered at %f pix, correcting this offset", ifuref,
2677  sliceref, sliceref + 12, xref);
2678  cpl_table_subtract_scalar(aGeo->table, MUSE_GEOTABLE_X, xref);
2679 
2680  return CPL_ERROR_NONE;
2681 } /* muse_geo_refine_horizontal() */
2682 
2683 /*----------------------------------------------------------------------------*/
2733 /*----------------------------------------------------------------------------*/
2736 {
2737  cpl_ensure(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT, NULL);
2738  int nrow = cpl_table_get_nrow(aGeo->table);
2739  cpl_ensure(nrow >= 10, CPL_ERROR_ILLEGAL_INPUT, NULL);
2740  cpl_ensure(muse_cpltable_check(aGeo->table, muse_geo_table_def) == CPL_ERROR_NONE,
2741  CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
2742  int spotmin = cpl_table_get_column_min(aGeo->table, "spot"),
2743  spotmax = cpl_table_get_column_max(aGeo->table, "spot");
2744  cpl_ensure(spotmin == spotmax, CPL_ERROR_ILLEGAL_INPUT, NULL);
2745  const double kScaleY = 60. / aGeo->scale / kMuseTypicalCubeSizeY;
2746 
2747  /* duplicate the input table, but already remove some useless lines */
2749  cpl_table_erase_column(gt->table, "dxerr");
2750  cpl_table_erase_column(gt->table, "dxl");
2751  cpl_table_erase_column(gt->table, "dxr");
2752  cpl_table_erase_column(gt->table, "xc");
2753  cpl_table_erase_column(gt->table, "yc");
2754  cpl_table_erase_column(gt->table, "dx");
2755  cpl_table_erase_column(gt->table, "flux");
2756  cpl_table_erase_column(gt->table, "lambda");
2757 
2758  /* set the vertical position value */
2759  // XXX this needs to take into account the angle and the relative shift of the middle spot
2760  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_Y, 0, nrow, 0.);
2761  cpl_table_add_columns(gt->table, MUSE_GEOTABLE_Y, "vpos");
2762  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y, "mm");
2763  cpl_table_fill_column_window_double(gt->table, MUSE_GEOTABLE_Y"err", 0, nrow, 0.);
2764  cpl_table_add_columns(gt->table, MUSE_GEOTABLE_Y"err", "vposerr");
2765  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y"err", "mm");
2766 #if 0
2767  printf("\nfull geometry table, with absolute \"y\" [mm]:\n");
2768  cpl_table_dump(gt->table, 0, nrow, stdout);
2769  fflush(stdout);
2770 #endif
2771 
2772  double maskangle = 0., fmaskrot = 1.;
2773  if (getenv("MUSE_GEOMETRY_MASK_ROTATION")) {
2774  maskangle = atof(getenv("MUSE_GEOMETRY_MASK_ROTATION"));
2775  fmaskrot = cos(maskangle * CPL_MATH_RAD_DEG);
2776  cpl_msg_warning(__func__, "Adapting to global mask rotation of %.4f deg "
2777  "(cos = %.4e)", maskangle, fmaskrot);
2778  }
2779  double pinholedy = kMuseCUmpmDY;
2780  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
2781  pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY"));
2782  cpl_msg_warning(__func__, "Using pinhole y distance of %f mm (instead of "
2783  "%f mm)", pinholedy, kMuseCUmpmDY);
2784  }
2785  cpl_boolean printgoing = getenv("MUSE_DEBUG_GEO_VERTICAL")
2786  && atoi(getenv("MUSE_DEBUG_GEO_VERTICAL")) > 0;
2787 
2788  unsigned short middleSlice;
2789  const unsigned char middleIFU = muse_geo_select_reference(aGeo, &middleSlice);
2790  cpl_msg_info(__func__, "Using IFU %hhu / SliceSky %hu as reference",
2791  middleIFU, middleSlice);
2792  double ycentral = NAN,
2793  ymax = -DBL_MAX, ymin = DBL_MAX;
2794  int irow;
2795  for (irow = 0; irow < nrow; irow++) {
2796  unsigned char ifu = cpl_table_get_int(gt->table, MUSE_GEOTABLE_FIELD, irow, NULL);
2797  unsigned short slice = cpl_table_get_int(gt->table, MUSE_GEOTABLE_SKY, irow, NULL);
2798  double y = cpl_table_get_double(gt->table, MUSE_GEOTABLE_Y, irow, NULL);
2799  if (ifu == middleIFU && slice == middleSlice) {
2800  ycentral = y;
2801  break;
2802  }
2803  if (y > ymax) {
2804  ymax = y;
2805  }
2806  if (y < ymin) {
2807  ymin = y;
2808  }
2809  } /* for irow (all table rows) */
2810  if (!isfinite(ycentral)) { /* average min and max */
2811  ycentral = (ymin + ymax) / 2.;
2812  cpl_msg_info(__func__, "Averaged the y range to ycentral = %f pix", ycentral);
2813  } else {
2814  cpl_msg_info(__func__, "Found IFU %hhu, slice %hu, using its y value as "
2815  "ycentral = %f pix", middleIFU, middleSlice, ycentral);
2816  }
2817  cpl_table_subtract_scalar(gt->table, MUSE_GEOTABLE_Y, ycentral);
2818  const unsigned short nsoff = kMuseSlicesPerCCD / 4, /* slice offset */
2819  nstack[] = { 3, 2, 4, 1, 0 };
2820  unsigned char i;
2821  for (i = 0; nstack[i] > 0; i++) {
2822  /* unselect middle-left slicer stack and delete the rest */
2823  cpl_table_unselect_all(gt->table);
2824  cpl_table_or_selected_int(gt->table, "stack", CPL_EQUAL_TO, nstack[i]);
2825  cpl_table *tstack = cpl_table_extract_selected(gt->table);
2826  int ntsrow = cpl_table_get_nrow(tstack);
2827  /* sort the stack table by subfield and then slice number on sky */
2828  cpl_propertylist *sorting = cpl_propertylist_new();
2829  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
2830  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
2831  cpl_table_sort(tstack, sorting);
2832  cpl_propertylist_delete(sorting);
2833  /* find the slice in the same slicer row as the reference slice from above */
2834  const unsigned short refslice = middleSlice - (nstack[i] - 3) * nsoff;
2835  cpl_table_select_all(tstack);
2836  cpl_table_and_selected_int(tstack, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, middleIFU);
2837  cpl_table_and_selected_int(tstack, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, refslice);
2838  if (cpl_table_count_selected(tstack) <= 0) {
2839  char *msg = cpl_sprintf("reference slice %hu of reference IFU %hhu not "
2840  "found in slicer stack %hu!", refslice,
2841  middleIFU, nstack[i]);
2842  cpl_msg_error(__func__, "%s", msg);
2843  cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "%s", msg);
2844  cpl_free(msg);
2845  cpl_table_delete(tstack);
2846  continue;
2847  }
2848  cpl_table_erase_selected(gt->table);
2849  cpl_array *asel = cpl_table_where_selected(tstack);
2850  int iref = cpl_array_get(asel, 0, NULL);
2851  cpl_array_delete(asel);
2852  double yref = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, iref, NULL);
2853  cpl_msg_info(__func__, "reference slice %hu of reference IFU %hhu found at row "
2854  "%d in slicer stack %hu!", refslice, middleIFU, iref, nstack[i]);
2855  /* Maybe the reference pinhole in this slicer stack was illuminated by a *
2856  * different pinhole than the one in the reference stack. Then we need to *
2857  * subtract one pinhole distance from the y-positions in this stack. */
2858  if (fabs(yref) > pinholedy / 2.) {
2859  cpl_msg_info(__func__, "%s vertical pinhole distance (%f) to "
2860  "recenter stack %hu", yref < 0. ? "adding" : "subtracting",
2861  pinholedy, nstack[i]);
2862  cpl_table_add_scalar(tstack, MUSE_GEOTABLE_Y, yref < 0. ? pinholedy : -pinholedy);
2863  yref = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, iref, NULL);
2864  }
2865  /* go back in the table from the reference slice, the input y values *
2866  * should decrease, the output y value are supposed to increase */
2867  cpl_table_new_column(tstack, "dy", CPL_TYPE_DOUBLE);
2868  cpl_table_set_column_unit(tstack, "dy", "mm");
2869  cpl_table_set_column_format(tstack, "dy", "%9.5f");
2870  cpl_table_set_double(tstack, "dy", iref, yref);
2871  /* keep the current y-position in a temporary column, "ycopy" */
2872  cpl_table_duplicate_column(tstack, "ycopy", tstack, MUSE_GEOTABLE_Y);
2873  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, iref, yref);
2874  double yprev = cpl_table_get_double(tstack, "ycopy", iref, NULL),
2875  dyoffset = pinholedy * fmaskrot,
2876  ysum = yref;
2877  int iprev;
2878  for (irow = iref - 1, iprev = iref; irow >= 0; iprev = irow, irow--) {
2879  /* see how many slices apart we are from the previous */
2880  int difu = cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, iprev, NULL)
2881  - cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, irow, NULL),
2882  dslice = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, iprev, NULL)
2883  - cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, irow, NULL);
2884  if (difu != 0) {
2885  dslice += nsoff;
2886  }
2887  double y = cpl_table_get_double(tstack, "ycopy", irow, NULL),
2888  dy = y - yprev, /* should be negative here, around -120 um */
2889  dexpect = dslice * kScaleY,
2890  dratio = dy / dexpect,
2891  dycor = 0.; /* correction to y distance, zero by default */
2892  if (dratio < -8.) { /* negative ratio here */
2893  dycor = 2 * dyoffset;
2894  } else if (dratio < -2.) {
2895  dycor = dyoffset;
2896  } else if (dratio > 5.) {
2897  dycor = -dyoffset;
2898  }
2899  if (printgoing) {
2900  cpl_msg_debug(__func__, "going back: %d %d, %f %f --> diff %9.6f "
2901  "expected %f ratio %9.6f --> dy = %f", difu, dslice,
2902  yprev, y, dy, dexpect, dratio, dy + dycor);
2903  }
2904  dy += dycor;
2905  if (abs(difu) > 1) { /* make a gap, of somewhat arbitrary size */
2906  double gap = abs(difu) * kMuseGeoIFUVGap;
2907  dy -= gap;
2908  cpl_msg_warning(__func__, "%d missing IFUs, subtracted %f from dy",
2909  abs(difu) - 1, gap);
2910  }
2911  cpl_table_set_double(tstack, "dy", irow, dy);
2912  ysum += dy;
2913  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, irow, ysum);
2914  yprev = y;
2915  } /* for irow (lower half of the stack table) */
2916  /* go forward in the table from the reference slice, the input y values *
2917  * should increase, the output y value are supposed to decrease */
2918  yprev = cpl_table_get_double(tstack, "ycopy", iref, NULL);
2919  ysum = yref; /* start summing with the first y step from the ref. slice */
2920  for (irow = iref + 1, iprev = iref; irow < ntsrow; iprev = irow, irow++) {
2921  int difu = cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, iprev, NULL)
2922  - cpl_table_get_int(tstack, MUSE_GEOTABLE_FIELD, irow, NULL),
2923  dslice = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, iprev, NULL)
2924  - cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, irow, NULL);
2925  if (difu != 0) {
2926  dslice -= nsoff;
2927  }
2928  double y = cpl_table_get_double(tstack, "ycopy", irow, NULL),
2929  dy = y - yprev, /* should be positive here, around +120 um */
2930  dexpect = -dslice * kScaleY,
2931  dratio = dy / dexpect,
2932  dycor = 0.; /* correction to y distance, zero by default */
2933  if (dratio > 8.) { /* positive ratio here */
2934  dycor = -2 * dyoffset;
2935  } else if (dratio > 2.) {
2936  dycor = -dyoffset;
2937  } else if (dratio < -5.) {
2938  dycor = dyoffset;
2939  }
2940  if (printgoing) {
2941  cpl_msg_debug(__func__, "going forward: %d %d, %f %f --> diff %9.6f "
2942  "expected %f ratio %9.6f --> dy = %f", difu, dslice,
2943  yprev, y, dy, dexpect, dratio, dy + dycor);
2944  }
2945  dy += dycor;
2946  if (abs(difu) > 1) { /* make a gap, of somewhat arbitrary size */
2947  double gap = abs(difu) * kMuseGeoIFUVGap;
2948  dy += gap;
2949  cpl_msg_warning(__func__, "%d missing IFUs, added %f to dy",
2950  abs(difu) - 1, gap);
2951  }
2952  cpl_table_set_double(tstack, "dy", irow, dy);
2953  ysum += dy;
2954  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, irow, ysum);
2955  yprev = y;
2956  } /* for irow (upper half of the stack table) */
2957  if (printgoing) {
2958  printf("\ngeometry table of slicer stack %hu, with \"dy\" [mm]:\n",
2959  nstack[i]);
2960  cpl_table_dump(tstack, 0, nrow, stdout);
2961  fflush(stdout);
2962  }
2963  /* done with the temporary columns "ycopy" and "dy" */
2964  cpl_table_erase_column(tstack, "ycopy");
2965  cpl_table_erase_column(tstack, "dy");
2966  /* insert this slicer stack data back into the main table */
2967  cpl_table_insert(gt->table, tstack, cpl_table_get_nrow(gt->table));
2968  cpl_table_delete(tstack);
2969  } /* for i (all slicer stacks in a particular order) */
2970 
2971  /* The above should have correctly derived the vertical central positions *
2972  * of the slices as seens from the data on the CCD. This does not work *
2973  * correctly for the slices at the top and/or bottom of each IFU, since *
2974  * they may lie within the shadow of the adjacent IFU. *
2975  * To correct this, go though all stacks in all IFUs separately, and try *
2976  * to reset the vertical distance between the two outermost slices to the *
2977  * average distance. */
2978  cpl_msg_info(__func__, "Correcting vertical position of top/bottom slices");
2979  unsigned char ifu;
2980  for (ifu = 1; ifu <= kMuseNumIFUs; ifu++) {
2981  for (i = 0; nstack[i] > 0; i++) {
2982  cpl_table_unselect_all(gt->table);
2983  cpl_table_or_selected_int(gt->table, "SubField", CPL_EQUAL_TO, ifu);
2984  cpl_table_and_selected_int(gt->table, "stack", CPL_EQUAL_TO, nstack[i]);
2985  if (!cpl_table_count_selected(gt->table)) {
2986  continue; /* next stack or IFU */
2987  }
2988  cpl_table *tstack = cpl_table_extract_selected(gt->table);
2989  int ntsrow = cpl_table_get_nrow(tstack);
2990  /* sort the stack table by sky slice number */
2991  cpl_propertylist *sorting = cpl_propertylist_new();
2992  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
2993  cpl_table_sort(tstack, sorting);
2994  cpl_propertylist_delete(sorting);
2995 
2996  /* first and last entry should now be the edge slices, but verify this */
2997  unsigned short nslicetop = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY, 0, NULL),
2998  nslicebot = cpl_table_get_int(tstack, MUSE_GEOTABLE_SKY,
2999  ntsrow - 1, NULL);
3000  cpl_boolean hastop = nslicetop % nsoff == 1,
3001  hasbot = nslicebot % nsoff == 0,
3002  haschanged = CPL_FALSE; /* if we changed the value(s) */
3003 #if 0
3004  printf("table of IFU %hhu / SliceSky %hu: %hu to %hu (%s, %s)\n",
3005  ifu, nstack[i], nslicetop, nslicebot,
3006  hastop ? "has top" : "does NOT have top",
3007  hasbot ? "has bottom" : "does NOT have bottom");
3008  cpl_table_dump(tstack, 0, 1000, stdout);
3009  fflush(stdout);
3010 #endif
3011 
3012  cpl_vector *vvpos = cpl_vector_new(ntsrow - 3),
3013  *vverr = cpl_vector_new(ntsrow - 3);
3014  for (irow = 1; irow < ntsrow - 2; irow++) {
3015  double dy = fabs(cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, irow + 1, NULL)
3016  - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, irow, NULL)),
3017  dye1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", irow, NULL),
3018  dye2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", irow + 1, NULL),
3019  dyerr = sqrt(dye1*dye1 + dye2*dye2);
3020  cpl_vector_set(vvpos, irow - 1, dy);
3021  cpl_vector_set(vverr, irow - 1, dyerr);
3022  } /* for irow (slices 2 .. 11 in this stack) */
3023  /* XXX should use a weighted mean but for now use the *
3024  * standard means of value and error vectors */
3025  double mean = cpl_vector_get_mean(vvpos),
3026  median = cpl_vector_get_median_const(vvpos),
3027  stdev = cpl_vector_get_stdev(vvpos),
3028  stdev2 = cpl_vector_get_mean(vverr);
3029  cpl_vector_delete(vvpos);
3030  cpl_vector_delete(vverr);
3031  cpl_msg_debug(__func__, "dy of IFU %hhu / SliceSky %hu: %f +/- %f (%f) [+/- %f]",
3032  ifu, nstack[i], mean, stdev, median, stdev2);
3033  if (hastop) {
3034  /* y-distance at top */
3035  double ytop = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, 0, NULL),
3036  dyt = fabs(ytop - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, 1, NULL)),
3037  dyt1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", 0, NULL),
3038  dyt2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", 1, NULL),
3039  dyterr = sqrt(dyt1*dyt1 + dyt2*dyt2);
3040  cpl_msg_debug(__func__, "dy of IFU %hhu / SliceSky %hu (top): %f +/- %f",
3041  ifu, nstack[i], dyt, dyterr);
3042  /* if the difference in distance is larger than the combined *
3043  * 1-sigma errors, then apply the difference as correction */
3044  if (mean - dyt > sqrt(dyterr*dyterr + stdev2*stdev2)) {
3045  /* add the difference to the y-position of the first (=top) slice */
3046  ytop += mean - dyt;
3047  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, 0, ytop);
3048  haschanged = CPL_TRUE;
3049 #if 0
3050  printf("new top entry (+%f):\n", mean - dyt);
3051  cpl_table_dump(tstack, 0, 1, stdout);
3052  fflush(stdout);
3053 #endif
3054  } /* if diff greater than error */
3055  } /* if hastop */
3056  if (hasbot) {
3057  /* y-distance (and error) at bottom */
3058  double ybot = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 1, NULL),
3059  dyb = fabs(ybot - cpl_table_get_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 2, NULL)),
3060  dyb1 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", ntsrow - 2, NULL),
3061  dyb2 = cpl_table_get_double(tstack, MUSE_GEOTABLE_Y"err", ntsrow - 1, NULL),
3062  dyberr = sqrt(dyb1*dyb1 + dyb2*dyb2);
3063  cpl_msg_debug(__func__, "dy of IFU %hhu / SliceSky %hu (bottom): %f +/- %f",
3064  ifu, nstack[i], dyb, dyberr);
3065  if (mean - dyb > sqrt(dyberr*dyberr + stdev2*stdev2)) {
3066  /* subtract the difference from the y-position of the last (=bottom) slice */
3067  ybot -= mean - dyb;
3068  cpl_table_set_double(tstack, MUSE_GEOTABLE_Y, ntsrow - 1, ybot);
3069  haschanged = CPL_TRUE;
3070 #if 0
3071  printf("new bottom entry (-%f):\n", mean - dyb);
3072  cpl_table_dump(tstack, ntsrow - 1, 1, stdout);
3073  fflush(stdout);
3074 #endif
3075  } /* if diff greater than error */
3076  } /* if hasbot */
3077 
3078  /* if we changed something, append the changed *
3079  * table portion back to the main gt->table */
3080  if (haschanged) {
3081  cpl_table_erase_selected(gt->table);
3082  cpl_table_insert(gt->table, tstack, cpl_table_get_nrow(gt->table));
3083  }
3084  cpl_table_delete(tstack);
3085  } /* for i (slicer stacks) */
3086  } /* for ifu */
3087 
3088  /* convert "y" column from [mm] to [pix] */
3089  cpl_table_divide_scalar(gt->table, MUSE_GEOTABLE_Y, kMuseSpaxelSizeY_WFM / aGeo->scale);
3090  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y, "pix");
3091  cpl_table_divide_scalar(gt->table, MUSE_GEOTABLE_Y"err", kMuseSpaxelSizeY_WFM / aGeo->scale);
3092  cpl_table_set_column_unit(gt->table, MUSE_GEOTABLE_Y"err", "pix");
3093 
3094  /* now we can delete the remaining useless lines in the output table */
3095  cpl_table_erase_column(gt->table, "spot");
3096  cpl_table_erase_column(gt->table, "xrel");
3097  cpl_table_erase_column(gt->table, "xrelerr");
3098  cpl_table_erase_column(gt->table, "vpos");
3099  cpl_table_erase_column(gt->table, "vposerr");
3100  cpl_table_erase_column(gt->table, "stack");
3101  return gt;
3102 } /* muse_geo_determine_vertical() */
3103 
3104 /*----------------------------------------------------------------------------*/
3137 /*----------------------------------------------------------------------------*/
3138 cpl_error_code
3140 {
3141  cpl_ensure_code(aGeo && aGeo->table, CPL_ERROR_NULL_INPUT);
3142  cpl_ensure_code(cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_FIELD) &&
3143  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_CCD) &&
3144  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_SKY) &&
3145  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_X) &&
3146  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_Y) &&
3147  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_ANGLE) &&
3148  cpl_table_has_column(aGeo->table, MUSE_GEOTABLE_WIDTH),
3149  CPL_ERROR_ILLEGAL_INPUT);
3150 
3151  /* correct the computed values by the wrong pinhole vertical distance */
3152  if (getenv("MUSE_GEOMETRY_PINHOLE_DY")) {
3153 #if 0
3154  FILE *fn1 = fopen("gt1.ascii", "w");
3155  fprintf(fn1, "geometry table before scaling\n");
3156  cpl_table_dump(aGeo->table, 0, 10000, fn1);
3157  fclose(fn1);
3158 #endif
3159  double pinholedy = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY")),
3160  fdy = kMuseCUmpmDY / pinholedy;
3161  cpl_msg_warning(__func__, "Pinhole y distance of %f mm was used instead of "
3162  "%f mm; scaling coordinates by %f!", pinholedy, kMuseCUmpmDY,
3163  fdy);
3164  int irow, nrow = cpl_table_get_nrow(aGeo->table);
3165  for (irow = 0; irow < nrow; irow++) {
3166  int err;
3167  /* scale the y coordinate directly */
3168  double y = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_Y, irow, &err);
3169  if (!err) {
3170  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_Y, irow, y * fdy);
3171  }
3172  /* Compute a vertical scale for the old angle, and scale that *
3173  * to compute the new angle, so that I don't have to use the *
3174  * approximate small-scale linearity of the tan() function. */
3175  double angleold = cpl_table_get_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, &err);
3176  if (!err) {
3177  double anglenew = atan(fdy * tan(angleold * CPL_MATH_RAD_DEG))
3178  * CPL_MATH_DEG_RAD;
3179  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, anglenew);
3180  }
3181  } /* for irow (all table rows) */
3182 #if 0
3183  FILE *fn2 = fopen("gt2.ascii", "w");
3184  fprintf(fn2, "geometry table after scaling by %f\n", fdy);
3185  cpl_table_dump(aGeo->table, 0, 10000, fn2);
3186  fclose(fn2);
3187 #endif
3188  } /* if */
3189 
3190  /* pad table with nonsense data for missing slices, but ignore missing IFUs */
3191  unsigned char nifu;
3192  for (nifu = 1; nifu <= kMuseNumIFUs; nifu++) {
3193  cpl_table_select_all(aGeo->table);
3194  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3195  if (cpl_table_count_selected(aGeo->table) < 1) {
3196  continue; /* this IFU is missing completely, skip it */
3197  }
3198 
3199  unsigned short nslice;
3200  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
3201  cpl_table_select_all(aGeo->table);
3202  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, nifu);
3203  cpl_table_and_selected_int(aGeo->table, MUSE_GEOTABLE_CCD, CPL_EQUAL_TO, nslice);
3204  if (cpl_table_count_selected(aGeo->table) > 0) {
3205  continue; /* this slice is there */
3206  }
3207  /* add this missing slice */
3208  cpl_table_set_size(aGeo->table, cpl_table_get_nrow(aGeo->table) + 1);
3209  int irow = cpl_table_get_nrow(aGeo->table) - 1;
3210  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_FIELD, irow, nifu);
3211  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_CCD, irow, nslice);
3212  cpl_table_set_int(aGeo->table, MUSE_GEOTABLE_SKY, irow,
3213  kMuseGeoSliceSky[nslice - 1]);
3214  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_X, irow, NAN);
3215  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_Y, irow, NAN);
3216  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_ANGLE, irow, 0.);
3217  cpl_table_set_double(aGeo->table, MUSE_GEOTABLE_WIDTH, irow, 0.);
3218  } /* for nslice */
3219  } /* for nifu */
3220 
3221  cpl_boolean needsnoflip = getenv("MUSE_GEOMETRY_NO_INVERSION")
3222  && atoi(getenv("MUSE_GEOMETRY_NO_INVERSION")) > 0;
3223  if (!needsnoflip) { /* just flip all slices */
3224  cpl_msg_info(__func__, "Flipping all slices because of data inversion!");
3225  cpl_table_multiply_scalar(aGeo->table, MUSE_GEOTABLE_Y, -1.);
3226  cpl_table_multiply_scalar(aGeo->table, MUSE_GEOTABLE_ANGLE, -1.);
3227  }
3228 
3229  /* sort the full table, by subfield and then slice number on sky */
3230  cpl_propertylist *sorting = cpl_propertylist_new();
3231  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
3232  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
3233  cpl_table_sort(aGeo->table, sorting);
3234  cpl_propertylist_delete(sorting);
3235 
3236 #if 0
3237  printf("\nfinal geometry table with all slicer stacks [pix]:\n");
3238  cpl_table_dump(aGeo->table, 0, nrow, stdout);
3239  fflush(stdout);
3240 #endif
3241 #if 0
3242  const char *fn = "GEOMETRY_TABLE.ascii";
3243  FILE *fp = fopen(fn, "w");
3244  fprintf(fp, "# final geometry table with all slicer stacks [pix]:\n");
3245  cpl_table_dump(aGeo->table, 0, nrow, fp);
3246  fclose(fp);
3247  cpl_msg_debug(__func__, "written geometry table in ASCII to \"%s\"", fn);
3248 #endif
3249 
3250  return CPL_ERROR_NONE;
3251 } /* muse_geo_finalize() */
3252 
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:1212
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:2359
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:99
cpl_image * data
the data extension
Definition: muse_image.h:46
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:1783
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:1629
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:1107
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:2735
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:804
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:795
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:1928
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:1011
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.
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:3139
muse_geo_centroid_type
Type of centroiding algorithm to use.
Definition: muse_geo.h:91
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:1334
void muse_geo_table_delete(muse_geo_table *aGeo)
Deallocate memory associated to a geometry table object.
Definition: muse_geo.c:1232
muse_geo_table * muse_geo_table_new(cpl_size aNRows, double aScale)
Create a new MUSE geometry table.
Definition: muse_geo.c:1191
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:1363
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:1043
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:1308