/* $Id: visir_spc_optmod.c,v 1.25 2012-08-03 01:55:22 llundin Exp $
 *
 * This file is part of the VISIR Pipeline
 * Copyright (C) 2002,2003 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02111-1307  USA
 */

/*
 * $Author: llundin $
 * $Date: 2012-08-03 01:55:22 $
 * $Revision: 1.25 $
 * $Name: not supported by cvs2svn $
 */

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

/*-----------------------------------------------------------------------------
                                   Includes
 -----------------------------------------------------------------------------*/

#include "visir_spc_optmod.h"

#include <stddef.h>
#include <math.h>
#include <assert.h>

/*----------------------------------------------------------------------------*/
/**
 * @defgroup visir_spc_optmod   Functions for VISIR spectroscopy optical model
 *
 * They can be used in the following way:
 * 1) Initialize the instrument-specific constants by calling
 *    visir_spc_optmod_init().
 * 2) Compute the wavelength-range by calling visir_spc_optmod_wlen().
 * 3) The dispersion is assumed to be linear, D = (wlmax-wlmin)/(npix-1), thus
 *    the wavelength on pixel i is wli = wlmin + i * D, i = 1, 2, ..., npix.
 *
 *  The below functionality is implemented according to
 *  "Parameters for setting the VISIR Spectrometer" rev. 2.4
 *  (VLT-TRE-VIS-14321-5046)
 *
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                                   Type definition
 -----------------------------------------------------------------------------*/

enum visir_spc_mode_ {
    VISIR_SPC_M_ERR = 0,
    VISIR_SPC_M_LSWN,
    VISIR_SPC_M_LLWN,
    VISIR_SPC_M_LSWQ,
    VISIR_SPC_M_LLWQ,
    VISIR_SPC_M_LRP,
    VISIR_SPC_M_MSWN,
    VISIR_SPC_M_MLWN,
    VISIR_SPC_M_MSWQ,
    VISIR_SPC_M_MLWQ,
    VISIR_SPC_M_GHR01,
    VISIR_SPC_M_GHR02,
    VISIR_SPC_M_GHR03,
    VISIR_SPC_M_GHR04,
    VISIR_SPC_M_GHR05,
    VISIR_SPC_M_GHR06,
    VISIR_SPC_M_GHR07,
    VISIR_SPC_M_GHR08,
    VISIR_SPC_M_GHR09,
    VISIR_SPC_M_GHR10,
    VISIR_SPC_M_GHR11,
    VISIR_SPC_M_GHR12,
    VISIR_SPC_M_GHR13,
    VISIR_SPC_M_GHR14,
    VISIR_SPC_M_GHR15,
    VISIR_SPC_M_GHR16,
    VISIR_SPC_M_GHR17,
    VISIR_SPC_M_GHR18,
    VISIR_SPC_M_GHR19,
    VISIR_SPC_M_GHR20,
    VISIR_SPC_M_GHR21,
    VISIR_SPC_M_GHR22,
    VISIR_SPC_M_GHR23,
    VISIR_SPC_M_GHR24,
    VISIR_SPC_M_GHR25,
    VISIR_SPC_M_GHR26,
    VISIR_SPC_M_GHR27,
    VISIR_SPC_M_HR01,
    VISIR_SPC_M_HR02,
    VISIR_SPC_M_HR03,
    VISIR_SPC_M_HR04,
    VISIR_SPC_M_HR05,
    VISIR_SPC_M_HR06,
    VISIR_SPC_M_HR07,
    VISIR_SPC_M_HR08,
    VISIR_SPC_M_HR09,
    VISIR_SPC_M_HR10
};

typedef enum visir_spc_mode_ visir_spc_mode;

struct visir_optmod_private_ {
    /* The Central Wavelength of the settings */
    double wlen;

    /* Angles are in radians */
    double angle_a;    /* Angle in 1st term */
    double angle_b0;   /* Angle in 2nd term, center of 1st pixel */
    double angle_bm;   /* Angle in 2nd term, center of spectral range */
    double angle_b1;   /* Angle in 2nd term, center of last pixel */

    double angle_scan; /* Scan-angle */
    double sinus_sum;  /* sin(a + scan-angle) + sin(b + scan-angle) */

    /* The unit of d has to be equal to that of the wavelength */
    double d;         /* Echelle groove spacing @30K [m] */

    /* These settings are only defined in High Resolution Grism mode, i.e. when
       mode == VISIR_SPC_R_GHR */
    double gg;        /* grism groove spacing @ 30K [m] */
    double w;         /* grism wedge [radians] */
    double offset;    /* Parameter for grism cross-dispersion */
    double factor;    /* Parameter for grism cross-dispersion */

    double dcolbeam;  /* Diameter of collimated beam */
    double ld;        /* The (unit-less) Linear Dispersion factor */

    /* The spectroscopy resolution corresponding to the settings */
    visir_spc_resol resolution;

    /* The spectroscopy mode corresponding to the settings */
    visir_spc_mode mode;

    int    m;         /* Echelle order (1, 2, ..., 18) */

    /* These settings are only defined in High Resolution, i.e. when
       mode == VISIR_SPC_R_GHR || mode == VISIR_SPC_R_HR */
    int side_is_A;    /* 1 if side is 'A', 0 if side is 'B' */

};

typedef struct visir_optmod_private_ visir_optmod_private;

/*-----------------------------------------------------------------------------
                            Functions prototypes
 -----------------------------------------------------------------------------*/

static double visir_spc_optmod_krs5(double);
static void visir_spc_optmod_scan_angle(visir_optmod_private *);

/**@{*/

/*-----------------------------------------------------------------------------
                            Function codes
 -----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/**
  @brief    Initialize VISIR spectroscopy instrument specific constants
  @param    resol The resolution ( VISIR_SPC_R_[[L|M|H|GH]R|RP] )
  @param    wlen  The central wavelength [m] (FIXME: ignored for _LRP ?)
  @param    pins  Pointer to instrument settings structure
  @param    is_aqu whether to build model for the aquarius detector
  @return   0 iff successful.

  The possible error conditions are:
  -1: pins is NULL
  -2: The given resolution does not support the given wavelength

 */
/*----------------------------------------------------------------------------*/
int visir_spc_optmod_init(visir_spc_resol resol, double wlen,
                          visir_optmod * pins, int is_aqu)
{

    visir_optmod_private * self = (visir_optmod_private*)pins;

    /* The angles are coded in degrees and then converted to radians */
    const double rad_per_deg = atan(1)/45;

    visir_spc_mode mode = VISIR_SPC_M_ERR;


    /* Ensure that the public struct has the proper size */
    assert( sizeof(visir_optmod_private) == sizeof(visir_optmod) );

    if (self == NULL) return -1;

    /* Switch on the resolution
       Constants here are coded in micron - convert to m */
    wlen *= 1e6;
    switch (resol) {
        case VISIR_SPC_R_LRP:
          {
              mode = VISIR_SPC_M_LRP;
              wlen = VISIR_SPC_LRP_CWLEN;
              break;
          }

        case VISIR_SPC_R_LR:
          {
                 if ( 7.5 <= wlen && wlen <= 10.2) mode = VISIR_SPC_M_LSWN;
            else if (10.2 <  wlen && wlen <= 14.5) mode = VISIR_SPC_M_LLWN;
            else if (15.0 <= wlen && wlen <= 20.4) mode = VISIR_SPC_M_LSWQ;
            else if (20.4 <  wlen && wlen <= 28.0) mode = VISIR_SPC_M_LLWQ;
            break;
          }

        case VISIR_SPC_R_MR:
          {
                 if ( 7.5 <= wlen && wlen <= 10.2) mode = VISIR_SPC_M_MSWN;
            else if (10.2 <  wlen && wlen <= 14.0) mode = VISIR_SPC_M_MLWN;
            else if (15.0 <= wlen && wlen <= 20.4) mode = VISIR_SPC_M_MSWQ;
            else if (20.4 <  wlen && wlen <= 28.0) mode = VISIR_SPC_M_MLWQ;
            break;
          }

        case VISIR_SPC_R_GHR:
          {
                 if ( 7.6 <= wlen && wlen <=  7.8 ) mode = VISIR_SPC_M_GHR01;
            else if ( 7.8  < wlen && wlen <=  8.03) mode = VISIR_SPC_M_GHR02;
            else if ( 8.03 < wlen && wlen <=  8.26) mode = VISIR_SPC_M_GHR03;
            else if ( 8.26 < wlen && wlen <=  8.52) mode = VISIR_SPC_M_GHR04;
            else if ( 8.52 < wlen && wlen <=  8.78) mode = VISIR_SPC_M_GHR05;
            else if ( 8.78 < wlen && wlen <=  9.07) mode = VISIR_SPC_M_GHR06;
            else if ( 9.07 < wlen && wlen <=  9.36) mode = VISIR_SPC_M_GHR07;
            else if ( 9.36 < wlen && wlen <=  9.69) mode = VISIR_SPC_M_GHR08;
            else if ( 9.69 < wlen && wlen <= 10.03) mode = VISIR_SPC_M_GHR09;
            else if (10.03 < wlen && wlen <= 10.20) mode = VISIR_SPC_M_GHR10;
            else if (10.2  < wlen && wlen <= 10.41) mode = VISIR_SPC_M_GHR11;
            else if (10.41 < wlen && wlen <= 10.80) mode = VISIR_SPC_M_GHR12;
            else if (10.80 < wlen && wlen <= 11.24) mode = VISIR_SPC_M_GHR13;
            else if (11.24 < wlen && wlen <= 11.70) mode = VISIR_SPC_M_GHR14;
            else if (11.70 < wlen && wlen <= 12.21) mode = VISIR_SPC_M_GHR15;
            else if (12.21 < wlen && wlen <= 12.76) mode = VISIR_SPC_M_GHR16;
            else if (12.76 < wlen && wlen <= 13.37) mode = VISIR_SPC_M_GHR17;
            else if (13.37 < wlen && wlen <= 14.04) mode = VISIR_SPC_M_GHR18;
            else if (14.04 < wlen && wlen <= 14.77) mode = VISIR_SPC_M_GHR19;
            else if (15.60 < wlen && wlen <= 16.49) mode = VISIR_SPC_M_GHR20;
            else if (16.49 < wlen && wlen <= 17.55) mode = VISIR_SPC_M_GHR21;
            else if (17.55 < wlen && wlen <= 18.67) mode = VISIR_SPC_M_GHR22;
            else if (18.67 < wlen && wlen <= 20.06) mode = VISIR_SPC_M_GHR23;
            else if (20.06 < wlen && wlen <= 21.49) mode = VISIR_SPC_M_GHR24;
            else if (21.49 < wlen && wlen <= 23.40) mode = VISIR_SPC_M_GHR25;
            else if (23.40 < wlen && wlen <= 25.32) mode = VISIR_SPC_M_GHR26;
            else if (25.32 < wlen && wlen <= 28.08) mode = VISIR_SPC_M_GHR27;
            break;
          }
        case VISIR_SPC_R_HR:
          {
                 if ( 7.97 <= wlen && wlen <=  8.27) mode = VISIR_SPC_M_HR01;
            else if ( 8.83 <= wlen && wlen <=  9.05) mode = VISIR_SPC_M_HR02;
            else if ( 9.52 <= wlen && wlen <=  9.72) mode = VISIR_SPC_M_HR03;
                 /* FIXME: VISIR_SPC_M_HR04 really goes up to 12.21 */
            else if (11.85 <= wlen && wlen <  12.19) mode = VISIR_SPC_M_HR04;
            else if (12.19 <= wlen && wlen <= 12.37) mode = VISIR_SPC_M_HR05;
            else if (12.37 <  wlen && wlen <= 12.71) mode = VISIR_SPC_M_HR06;
            else if (12.71 <  wlen && wlen <= 12.91) mode = VISIR_SPC_M_HR07;
            else if (16.80 <= wlen && wlen <= 17.20) mode = VISIR_SPC_M_HR08;
            else if (18.32 <= wlen && wlen <= 18.67) mode = VISIR_SPC_M_HR09;
            else if (18.67 <  wlen && wlen <= 19.18) mode = VISIR_SPC_M_HR10;
            break;
          }
        default:;
    }
    wlen *= 1e-6;

    if (mode == VISIR_SPC_M_ERR) return -2;

    self->resolution = resol;
    self->mode = mode;
    self->wlen = wlen;
    self->m = 0;

    self->angle_a  = 0;
    self->angle_b0 = 0;
    self->angle_bm = 0;
    self->angle_b1 = 0;

    /* Switch on the resolution */
    switch (resol) {
    case VISIR_SPC_R_LRP:
        {
            break;
        }
    case VISIR_SPC_R_LR:
        {
            self->angle_a  = 6.708;
            self->angle_bm = 1.291;
            self->angle_b0 = 0.586;
            self->angle_b1 = 2.0;
            self->dcolbeam = 53000; /* [micron] */
            self->ld = 10332;

            /* Switch on the (LR)-mode */
            switch (mode) {
            case VISIR_SPC_M_LSWN:
                {
                    self->d = 129.162;
                    self->m = 2;
                    break;
                }
            case VISIR_SPC_M_LLWN:
                {
                    self->d = 172.308;
                    self->m = 2;
                    break;
                }
            case VISIR_SPC_M_LSWQ:
                {
                    self->d = 129.162;
                    self->m = 1;
                    break;
                }
            case VISIR_SPC_M_LLWQ:
                {
                    self->d = 172.308;
                    self->m = 1;
                    break;
                }
            default:;
            }
            break;
        }
    case VISIR_SPC_R_MR:
        {
            self->angle_a  = 34.208;
            self->angle_bm = 28.791;
            self->angle_b0 = 28.086;
            self->angle_b1 = 29.500;
            self->dcolbeam = 53000; /* [micron] */
            self->ld = 10332;

            /* Switch on the (MR)-mode */
            switch (mode) {
            case VISIR_SPC_M_MSWN:
                {
                    self->d = 17.1478;
                    self->m = 2;
                    break;
                }
            case VISIR_SPC_M_MLWN:
                {
                    self->d = 22.9560;
                    self->m = 2;
                    break;
                }
            case VISIR_SPC_M_MSWQ:
                {
                    self->d = 17.1478;
                    self->m = 1;
                    break;
                }
            case VISIR_SPC_M_MLWQ:
                {
                    self->d = 22.9560;
                    self->m = 1;
                    break;
                }
            default:;
            }
            break;
        }
    case VISIR_SPC_R_GHR:
        {
            /* Switch on the (HR Grism)-mode */
            self->side_is_A = 0;
            self->dcolbeam = 125000; /* [micron] */
            self->ld = 23403;

            switch (mode) {
            case VISIR_SPC_M_GHR01:
                {
                    self->d=77.16526;
                    self->m=18;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=166.9;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR02:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=18;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=178;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR03:
                {
                    self->d=77.16526;
                    self->m=17;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=166.9;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR04:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=17;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=178;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR05:
                {
                    self->d=77.16526;
                    self->m=16;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=166.9;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR06:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=16;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=178;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR07:
                {
                    self->d=77.16526;
                    self->m=15;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=166.9;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR08:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=15;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=178;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR09:
                {
                    self->d=77.16526;
                    self->m=14;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=166.9;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR10:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=14;
                    self->gg=36.8906;
                    self->w=9.8;
                    self->offset=178;
                    self->factor=2940;
                    break;
                }

            case VISIR_SPC_M_GHR11:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=14;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=143.1;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR12:
                {
                    self->d=77.16526;
                    self->m=13;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=131.6;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR13:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=13;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=143.1;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR14:
                {
                    self->d=77.16526;
                    self->m=12;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=131.6;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR15:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=12;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=143.1;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR16:
                {
                    self->d=77.16526;
                    self->m=11;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=131.6;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR17:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=11;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=143.1;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR18:
                {
                    self->d=77.16526;
                    self->m=10;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=131.6;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR19:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=10;
                    self->gg=63.5470;
                    self->w=7.6;
                    self->offset=143.1;
                    self->factor=3004;
                    break;
                }

            case VISIR_SPC_M_GHR20:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=9;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=120.3;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR21:
                {
                    self->d=77.16526;
                    self->m=8;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=108.7;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR22:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=8;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=120.3;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR23:
                {
                    self->d=77.16526;
                    self->m=7;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=108.7;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR24:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=7;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=120.3;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR25:
                {
                    self->d=77.16526;
                    self->m=6;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=108.7;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR26:
                {
                    self->side_is_A = 1;
                    self->d=79.93104;
                    self->m=6;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=120.3;
                    self->factor=2980;
                    break;
                }

            case VISIR_SPC_M_GHR27:
                {
                    self->d=77.16526;
                    self->m=5;
                    self->gg=217.8772;
                    self->w=4.1;
                    self->offset=108.7;
                    self->factor=2980;
                    break;
                }
            default:;
            }

            if (self->side_is_A) {
                /* side 'A' */
                self->angle_a  = 62.1299;
                self->angle_bm = 64.8519;
                self->angle_b0 = 64.5393;
                self->angle_b1 = 65.1641;

            } else {
                /* side 'B' */
                self->angle_a  = 64.8701;
                self->angle_bm = 62.1483;
                self->angle_b0 = 62.4609;
                self->angle_b1 = 61.8361;
            }
            self->w *= rad_per_deg;

            /* Length constants are coded in micron - convert to m */
            self->gg *= 1e-6;
            break;
        }
    case VISIR_SPC_R_HR:
        {
            /* Switch on the (HR Long Slit)-mode */
            self->side_is_A = 0;
            self->dcolbeam = 125000; /* [micron] */
            self->ld = 23403;

            switch (mode) {
            case VISIR_SPC_M_HR01:
                {
                    self->m=17;
                    break;
                }
            case VISIR_SPC_M_HR02:
                {
                    self->side_is_A = 1;
                    self->m=16;
                    break;
                }
            case VISIR_SPC_M_HR03:
                {
                    self->side_is_A = 1;
                    self->m=15;
                    break;
                }
            case VISIR_SPC_M_HR04:
                {
                    self->side_is_A = 1;
                    self->m=12;
                    break;
                }
            case VISIR_SPC_M_HR05:
                {
                    self->m=11;
                    break;
                }
            case VISIR_SPC_M_HR06:
                {
                    self->m=11;
                    break;
                }
            case VISIR_SPC_M_HR07:
                {
                    self->side_is_A = 1;
                    self->m=11;
                    break;
                }
            case VISIR_SPC_M_HR08:
                {
                    self->m=8;
                    break;
                }
            case VISIR_SPC_M_HR09:
                {
                    self->side_is_A = 1;
                    self->m=8;
                    break;
                }
            case VISIR_SPC_M_HR10:
                {
                    self->m=7;
                    break;
                }
            default:;
            }
            if (self->side_is_A) {
                /* side 'A' */
                self->d=79.93104;
                self->angle_a  = 62.1299;
                self->angle_bm = 64.8519;
                self->angle_b0 = 64.5393;
                self->angle_b1 = 65.1641;
            } else {
                /* side 'B' */
                self->d=77.16526;
                self->angle_a  = 64.8701;
                self->angle_bm = 62.1483;
                self->angle_b0 = 62.4609;
                self->angle_b1 = 61.8361;
            }
            break;
        }
    default:;
    }

    if (is_aqu && (resol == VISIR_SPC_R_HR || resol == VISIR_SPC_R_GHR)) {
        /* adapt VLT-TRE-VIS-14321-5046 measurements to new detector size and
         * pfov */
        self->factor *= 0.127 / 0.0757;
        self->ld *= 0.127 / 0.0757;
        /* drs: b0 at pixel 1, bm at 128.5, b1 at 256 with pfov 0.127
         * aqu: bm at 512.5, b1 at 1024 with pfov 0.757
         * => d = 128.5 / tan(b1 - bm) -> (b1 - bm)_new = atan((512.5 / 1.6777) / d) */
        if (self->angle_b0 < self->angle_bm) { /* A side */
            self->angle_b0 = self->angle_bm - (self->angle_bm - self->angle_b0) * 2.377;
            self->angle_b1 = self->angle_bm + (self->angle_b1 - self->angle_bm) * 2.377;
        }
        else { /* B side */
            self->angle_b0 = self->angle_bm + (self->angle_b0 - self->angle_bm) * 2.377;
            self->angle_b1 = self->angle_bm - (self->angle_bm - self->angle_b1) * 2.377;
        }
        self->offset += 162;
    }

    if (resol != VISIR_SPC_R_LRP) {
        self->angle_a  *= rad_per_deg;
        self->angle_b0 *= rad_per_deg;
        self->angle_bm *= rad_per_deg;
        self->angle_b1 *= rad_per_deg;

        /* Length constants are coded in micron - convert to m */
        self->d        *= 1e-6;
        self->dcolbeam *= 1e-6;

        assert( self->m > 0);

        visir_spc_optmod_scan_angle(self);

        self->sinus_sum = sin(self->angle_a  + self->angle_scan)
            + sin(self->angle_bm + self->angle_scan);
    }

    return 0;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the wavelengths based on the optical model
  @param    pins  Pointer to instrument settings structure
  @param    pwl0  Iff not NULL, the wavelength on the 1'st pixel [m]
  @param    pwl1  Iff not NULL, the wavelength on the last pixel [m]
  @return   The Central Wavelength [m] or a negative number on error

  pins must point to a struct initialized by a call to visir_spc_optmod_init().

  The possible error conditions are:
  -1: pins is NULL

 */
/*----------------------------------------------------------------------------*/
double visir_spc_optmod_wlen(const visir_optmod * pins, double * pwl0, 
                             double * pwl1)
{

    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    if (self->mode == VISIR_SPC_M_LRP) {
        if (pwl0) *pwl0 = VISIR_SPC_LRP_WLEN0;
        if (pwl1) *pwl1 = VISIR_SPC_LRP_WLEN1;
        return VISIR_SPC_LRP_CWLEN;
    }

    /* The wavelength on the center of the 1st pixel */
    if (pwl0) *pwl0 = self->d/self->m*( sin(self->angle_a  + self->angle_scan)
                                      + sin(self->angle_b0 + self->angle_scan));
  
    /* The wavelength on the center of the last pixel */
    if (pwl1) *pwl1 = self->d/self->m*( sin(self->angle_a  + self->angle_scan)
                                      + sin(self->angle_b1 + self->angle_scan));

    /* The wavelength on the detector-center according to the model */
    return self->d / self->m * self->sinus_sum;

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the HR Grism Cross-dispersion [pixel]
  @param    pins  Pointer to instrument settings structure
  @param    wlen  The wavelength [m]
  @return   The HR Grism Cross-dispersion [pixel] or a negative number on error

  pins must point to a struct initialized by a call to
  visir_spc_optmod_init(VISIR_SPC_R_GHR, ...).

  The possible error conditions are:
  -1: pins is NULL
  -2: Illegal resolution
  -3: Non-positive wlen
  -8: Unsupported wlen
  -9: Unsupported wlen (too big)
  -10: Unsupported wlen (too small)

 */
/*----------------------------------------------------------------------------*/
double visir_spc_optmod_cross_dispersion(const visir_optmod * pins, double wlen)
{

    double sinbeta;
    double rf_index;
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    if (self->resolution != VISIR_SPC_R_GHR) return -2;
    if (wlen <= 0) return -3;

    assert( self->gg != 0 );

    rf_index = visir_spc_optmod_krs5(wlen);

    if (rf_index < 1) return -8;

    sinbeta = sin(self->w) * rf_index - wlen / self->gg;

    /* Correct any rounding errors */
    if (sinbeta < -1) return -9;
    if (sinbeta >  1) return -10;

    return self->offset + self->factor * tan(asin(sinbeta) - self->w);

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the HR Grism wavelength at the order offset by ioffset.
  @param    pins       Pointer to instrument settings structure
  @param    wlen       Wavelength to be scaled
  @param    ioffset    HR Grism order offset (0 for main)
  @return   The central wavelength at the offset or a negative number on error

  wlen may also be a dispersion coefficient.

  pins must point to a struct initialized by a call to
  visir_spc_optmod_init(VISIR_SPC_R_GHR, ...).

  The possible error conditions are:
  -1: pins is NULL
  -2: Illegal resolution
  -3: Non-positive wlen
  -4: Order-offset too small
  -5: Order-offset too big
  -6: Offset too small
  -7: Offset too big

 */
/*----------------------------------------------------------------------------*/
double visir_spc_optmod_echelle(const visir_optmod * pins, double wlen,
                                int ioffset)
{

    int order;
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;
    if (self->resolution != VISIR_SPC_R_GHR) return -2;
    if (wlen <= 0) return -3;
    if (ioffset < -4) return -4;
    if (ioffset >  4) return -5;

    order = ioffset + self->m;

    /* There are 18 echelle orders */
    if (order <    1) return -6;
    if (order >   18) return -7;

    return wlen * self->m / (double) order;

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Determine whether or not the HR side is 'A' (or 'B')
  @param    pins  Pointer to instrument settings structure
  @return   1 if the side is 'A', 0 if not, and negative on error.

  pins must point to a struct initialized by a call to
  visir_spc_optmod_init(VISIR_SPC_R_GHR, ...) or
  visir_spc_optmod_init(VISIR_SPC_R_HR, ...).

  The possible error conditions are:
  -1: pins is NULL
  -2: Illegal resolution

 */
/*----------------------------------------------------------------------------*/
int visir_spc_optmod_side_is_A(const visir_optmod * pins)
{
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    if (self->resolution != VISIR_SPC_R_GHR &&
        self->resolution != VISIR_SPC_R_HR) return -2;

    return self->side_is_A;

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Determine the HR Grism echelle order
  @param    pins  Pointer to instrument settings structure
  @return   The echelle order or a negative number on error.

  pins must point to a struct initialized by a call to
  visir_spc_optmod_init(VISIR_SPC_R_GHR, ...) or
  visir_spc_optmod_init(VISIR_SPC_R_HR, ...).

  The possible error conditions are:
  -1: pins is NULL
  -2: Illegal resolution

 */
/*----------------------------------------------------------------------------*/
int visir_spc_optmod_get_echelle_order(const visir_optmod * pins)
{
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    if (self->resolution != VISIR_SPC_R_GHR &&
        self->resolution != VISIR_SPC_R_HR) return -2;

    return self->m;
}


/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the Spectral Resolution at the central wavelength.
  @param    pins  Pointer to instrument settings structure
  @return   The Spectral Resolution or a negative number on error

  pins must point to a struct initialized by a call to visir_spc_optmod_init().

  The possible error conditions are:
  -1: pins is NULL

 */
/*----------------------------------------------------------------------------*/

double visir_spc_optmod_resolution(const visir_optmod * pins)
{
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    return self->mode == VISIR_SPC_M_LRP
        ? VISIR_SPC_LRP_RESOL
        : self->dcolbeam * self->sinus_sum
        / (2.0 * self->wlen * cos(self->angle_a  + self->angle_scan));
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the Linear Dispersion at the central wavelength.
  @param    pins  Pointer to instrument settings structure
  @return   The Linear Dispersion or a negative number on error
  @note The unit is pixel/meter.

  pins must point to a struct initialized by a call to visir_spc_optmod_init().

  The possible error conditions are:
  -1: pins is NULL

 */
/*----------------------------------------------------------------------------*/

double visir_spc_optmod_dispersion(const visir_optmod * pins)
{
    const visir_optmod_private * self = (const visir_optmod_private*)pins;

    if (self == NULL) return -1;

    return self->mode == VISIR_SPC_M_LRP
        ? VISIR_SPC_LRP_RESOL_DISP
        : self->ld * self->sinus_sum
        / (self->wlen * cos(self->angle_bm + self->angle_scan));
}

/**@}*/

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the refractive index n of KRS5
  @param    wlen  the wavelength [m]
  @return   The refractive index n of KRS5 (greater than 1), or -1 on error.

 */
/*----------------------------------------------------------------------------*/
static double visir_spc_optmod_krs5(double wlen) {

    const double a0 =  5.96032159;
    const double a1 = -5.36135205e-4;
    const double a2 =  1.77047634;
    const double a3 = -2.79310980e1;
    const double a4 = -1.28684883;
    const double a5 = -4.34541795e-2;

    double n2 = 0;


    assert( wlen > 0 );

    /* The above constants are for a wavelength in microns */
    wlen *= 1e6;

    wlen *= wlen;

    n2 = a0 + a1 * wlen + (((a5/wlen + a4)/wlen + a3)/ wlen + a2)/wlen;

    /* The refractive index is greater than 1 */
    return n2 > 1 ? sqrt(n2) : -1;

}

/*----------------------------------------------------------------------------*/
/**
  @brief    Compute the Scan Angle from the instrument settings
  @param    self  Pointer to instrument settings structure
  @return   void

 */
/*----------------------------------------------------------------------------*/
static void visir_spc_optmod_scan_angle(visir_optmod_private * self)
{

    /* Determine the Scan-angle from Mode and Central Wavelength */

    const double mld = self->m * self->wlen / self->d;
    const double sab = sin(self->angle_bm) + sin(self->angle_a);
    const double cab = cos(self->angle_bm) + cos(self->angle_a);
    const double A = sab * sab + cab * cab;
    /*
      const double B = -2 * mld * cab;
    */
    const double C = mld * mld - sab * sab;

    double D = A - mld * mld; /* D = B^2 - 4 * A * C */
    double u1, u2;

    /* D can only become negative due to rounding errors */
    D = D > 0 ? sqrt(D) : 0;
    D *= fabs(sab);

    /* A, cab, mld & D are all positive */
    u1 = (cab * mld + D) / A;

    /* u2 may be a lot smaller than both D/A and cab * mld/A, so subtraction
       of those two terms may lead to loss of precision. u2 is instead
       computed by use of the fact that C = A * u1 * u2 */
    u2 = C / A / u1;           

    /* u1 is not a physically meaningful solution, it is just an
       intermediate result */

    self->angle_scan = asin(u2);

}
