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