#!/usr/bin/env python

"""main Absorb routines
   Copyright: 2009, Robert B. Von Dreele (Argonne National Laboratory)
"""
from __future__ import division, print_function
import platform
import math
import wx
import numpy as np
import sys
import matplotlib as mpl
import GSASIIpath
GSASIIpath.SetVersionNumber("$Revision: 4672 $")
import GSASIIElem as G2elem
import GSASIIElemGUI as G2elemGUI

try:
    wx.NewIdRef
    wx.NewId = wx.NewIdRef
except AttributeError:
    pass

if '2' in platform.python_version_tuple()[0]:
    Gktheta = unichr(0x3b8)
    Gklambda = unichr(0x3bb)
    GkDelta = unichr(0x0394)
    Pwr10 = unichr(0x0b9)+unichr(0x2070)
    Pwr20 = unichr(0x0b2)+unichr(0x2070)
    Pwrm1 = unichr(0x207b)+unichr(0x0b9)
    Pwrm2 = unichr(0x207b)+unichr(0x0b2)
    Pwrm6 = unichr(0x207b)+unichr(0x2076)
    Pwrm4 = unichr(0x207b)+unichr(0x2074)
    Angstr = unichr(0x00c5)
    Gkmu = unichr(0x3bc)
    Pwr3 = unichr(0x0b3)
    Pwr4 = unichr(0x2074)
    Pwr20 = unichr(0x0b2)+unichr(0x0b0)
    Pwrm1 = unichr(0x207b)+unichr(0x0b9)

else:
    Gktheta = chr(0x3b8)
    Gklambda = chr(0x3bb)
    GkDelta = chr(0x0394)
    Pwr10 = chr(0x0b9)+chr(0x2070)
    Pwr20 = chr(0x0b2)+chr(0x2070)
    Pwrm1 = chr(0x207b)+chr(0x0b9)
    Pwrm2 = chr(0x207b)+chr(0x0b2)
    Pwrm6 = chr(0x207b)+chr(0x2076)
    Pwrm4 = chr(0x207b)+chr(0x2074)
    Angstr = chr(0x00c5)
    Gkmu = chr(0x3bc)
    Pwr3 = chr(0x0b3)
    Pwr4 = chr(0x2074)
    Pwr20 = chr(0x0b2)+chr(0x0b0)
    Pwrm1 = chr(0x207b)+chr(0x0b9)

[wxID_CHOICE1, wxID_SPINTEXT1, wxID_SPINTEXT2, wxID_SPINTEXT3, wxID_SPINTEXT4,
 wxID_RESULTS,wxID_SLIDER1, wxID_SPINBUTTON, wxID_NUMELEM, wxID_SPINTEXT5,wxID_SPINTEXT6,
] = [wx.NewId() for _init_ctrls in range(11)]

[wxID_EXIT, wxID_DELETE, wxID_NEW,
] = [wx.NewId() for _init_coll_ABSORB_Items in range(3)]

[wxID_KALPHAAGKA, wxID_KALPHACOKA, wxID_KALPHACRKA,
 wxID_KALPHACUKA, wxID_KALPHAFEKA, wxID_KALPHAMNKA,
 wxID_KALPHAMOKA, wxID_KALPHANIKA, wxID_KALPHAZNKA,
] = [wx.NewId() for _init_coll_KALPHA_Items in range(9)]

[wxID_ABSORBABOUT] = [wx.NewId() for _init_coll_ABOUT_Items in range(1)]

class Absorb(wx.Frame):
    ''' '''
    Elems = []
    Wave = 1.5405      #CuKa default
    Kev = 12.397639    #keV for 1A x-rays
    for arg in sys.argv:
        if '-w' in arg:
            Wave = float(arg.split('-w')[1])
        elif '-e' in arg:
            E = float(arg.split('-e')[1])
            Wave = Kev/E
        elif '-h' in arg:
            print ('''
Absorb.py can take the following arguments:
-h   -  this help listing
-wv  -  set default wavelength to v, e.g. -w1.54 sets wavelength to 1.54A
-ev  -  set default energy to v, e.g. -e27 sets energy to 27keV
without arguments Absorb uses CuKa as default (Wave=1.54052A, E=8.0478keV)
''')
            sys.exit()
    Wmin = 0.05        #wavelength range
    Wmax = 3.0
    Wres = 0.004094    #plot resolution step size as const delta-lam/lam - gives 1000 steps for Wmin to Wmax
    Eres = 1.5e-4      #typical energy resolution for synchrotron x-ray sources
    Energy = Kev/Wave
    ifWave = True
    Volume = 0
    ifVol = False
    Zcell = 1
    Pack = 0.50
    Radius = 0.4
    def _init_coll_ABOUT_Items(self, parent):

        parent.Append(wxID_ABSORBABOUT,'About')
        self.Bind(wx.EVT_MENU, self.OnABOUTItems0Menu, id=wxID_ABSORBABOUT)

    def _init_coll_menuBar1_Menus(self, parent):

        parent.Append(menu=self.ABSORB, title='Absorb')
        parent.Append(menu=self.KALPHA, title='Kalpha')
        parent.Append(menu=self.ABOUT, title='About')

    def _init_coll_KALPHA_Items(self, parent):
        "Set of characteristic radiation from sealed tube sources"
        def OnCrkaMenu(event):
            self.SetWaveEnergy(2.28962)

        def OnMnkaMenu(event):
            self.SetWaveEnergy(2.10174)

        def OnFekaMenu(event):
            self.SetWaveEnergy(1.93597)

        def OnCokaMenu(event):
            self.SetWaveEnergy(1.78896)

        def OnNikaMenu(event):
            self.SetWaveEnergy(1.65784)

        def OnCukaMenu(event):
            self.SetWaveEnergy(1.54052)

        def OnZnkaMenu(event):
            self.SetWaveEnergy(1.43510)

        def OnMokaMenu(event):
            self.SetWaveEnergy(0.70926)

        def OnAgkaMenu(event):
            self.SetWaveEnergy(0.55936)

        parent.Append(wxID_KALPHACRKA, 'CrKa')
        parent.Append(wxID_KALPHAMNKA, 'MnKa')
        parent.Append(wxID_KALPHAFEKA, 'FeKa')
        parent.Append(wxID_KALPHACOKA, 'CoKa')
        parent.Append(wxID_KALPHANIKA, 'NiKa')
        parent.Append(wxID_KALPHACUKA, 'CuKa')
        parent.Append(wxID_KALPHAZNKA, 'ZnKa')
        parent.Append(wxID_KALPHAMOKA, 'MoKa')
        parent.Append(wxID_KALPHAAGKA, 'AgKa')
        self.Bind(wx.EVT_MENU, OnCrkaMenu, id=wxID_KALPHACRKA)
        self.Bind(wx.EVT_MENU, OnMnkaMenu, id=wxID_KALPHAMNKA)
        self.Bind(wx.EVT_MENU, OnFekaMenu, id=wxID_KALPHAFEKA)
        self.Bind(wx.EVT_MENU, OnCokaMenu, id=wxID_KALPHACOKA)
        self.Bind(wx.EVT_MENU, OnNikaMenu, id=wxID_KALPHANIKA)
        self.Bind(wx.EVT_MENU, OnCukaMenu, id=wxID_KALPHACUKA)
        self.Bind(wx.EVT_MENU, OnZnkaMenu, id=wxID_KALPHAZNKA)
        self.Bind(wx.EVT_MENU, OnMokaMenu, id=wxID_KALPHAMOKA)
        self.Bind(wx.EVT_MENU, OnAgkaMenu, id=wxID_KALPHAAGKA)

    def _init_coll_ABSORB_Items(self, parent):
        parent.Append(wxID_NEW,'&New Element','Add new element')
        self.Delete = parent.Append(wxID_DELETE,'&Delete Element','Delete an element')
        self.Delete.Enable(False)
        parent.Append(wxID_EXIT,'&Exit','Exit Fprime')
        self.Bind(wx.EVT_MENU, self.OnExitMenu, id=wxID_EXIT)
        self.Bind(wx.EVT_MENU, self.OnNewMenu, id=wxID_NEW)
        self.Bind(wx.EVT_MENU, self.OnDeleteMenu, id=wxID_DELETE)

    def _init_utils(self):
        self.ABSORB = wx.Menu(title='')

        self.KALPHA = wx.Menu(title='')
        self.KALPHA.SetEvtHandlerEnabled(True)

        self.ABOUT = wx.Menu(title='')

        self.menuBar1 = wx.MenuBar()

        self._init_coll_ABSORB_Items(self.ABSORB)
        self._init_coll_KALPHA_Items(self.KALPHA)
        self._init_coll_ABOUT_Items(self.ABOUT)
        self._init_coll_menuBar1_Menus(self.menuBar1)

    def _init_ctrls(self, parent):
        wx.Frame.__init__(self, parent=parent,
              size=wx.Size(500, 400),style=wx.DEFAULT_FRAME_STYLE ^ wx.CLOSE_BOX, title='Absorb')
        self._init_utils()
        self.SetMenuBar(self.menuBar1)
        self.DrawPanel()

    def SetSize(self):
        w,h = self.GetClientSize()
        self.panel.SetSize(wx.Size(w,h))

    def DrawPanel(self):
        self.panel = wx.Panel(self)

        mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.Results = wx.TextCtrl( parent=self.panel,
            style=wx.TE_MULTILINE|wx.TE_DONTWRAP )
        self.Results.SetEditable(False)
        mainSizer.Add(self.Results,1,wx.EXPAND)
        mainSizer.Add((10,15),0)

        if self.Elems:
            lablSizer = wx.BoxSizer(wx.HORIZONTAL)
            lablSizer.Add((5,10),0)
            lablSizer.Add(wx.StaticText(parent=self.panel,label='Chemical Formula:'),0,
                wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT)
            mainSizer.Add(lablSizer,0)
            mainSizer.Add((5,5),0)
            nRow = len(self.Elems)/5
            compSizer = wx.FlexGridSizer(nRow+1,10,0,0)
            for Elem in self.Elems:
                compSizer.Add(wx.StaticText(parent=self.panel,label="  "+Elem[0].capitalize(),
                    size=wx.Size(30,20)),0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT)
                numElem = wx.TextCtrl(id=wxID_NUMELEM,parent=self.panel,name=Elem[0],
                    size=wx.Size(70,20),value="%.2f" % (Elem[2]),style=wx.TE_PROCESS_ENTER)
                compSizer.Add(numElem,0)
                numElem.Bind(wx.EVT_TEXT_ENTER, self.OnNumElem, id=wxID_NUMELEM)
            mainSizer.Add(compSizer,0)
            mainSizer.Add((10,15),0)

        selSizer = wx.BoxSizer(wx.HORIZONTAL)
        selSizer.Add((5,10),0)
        selSizer.Add(wx.StaticText(parent=self.panel, label='Wavelength:'),0,wx.EXPAND)
        selSizer.Add((5,10),0)
        self.SpinText1 = wx.TextCtrl(id=wxID_SPINTEXT1, parent=self.panel,
            size=wx.Size(100,20), value = "%.4f" % (self.Wave),style=wx.TE_PROCESS_ENTER )
        selSizer.Add(self.SpinText1,0)
        selSizer.Add((5,10),0)
        self.SpinText1.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText1, id=wxID_SPINTEXT1)

        selSizer.Add(wx.StaticText(parent=self.panel, label='Energy:'),0,wx.EXPAND)
        selSizer.Add((5,10),0)
        self.SpinText2 = wx.TextCtrl(id=wxID_SPINTEXT2, parent=self.panel,
            size=wx.Size(100,20), value = "%.4f" % (self.Energy),style=wx.TE_PROCESS_ENTER)
        selSizer.Add(self.SpinText2,0)
        selSizer.Add((5,10),0)
        self.SpinText2.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText2, id=wxID_SPINTEXT2)

        selSizer.Add(wx.StaticText(parent=self.panel, label='Plot scale:'),0,wx.EXPAND)
        selSizer.Add((5,10),0)
        self.choice1 = wx.ComboBox(id=wxID_CHOICE1, parent=self.panel, value='Wavelength',
             choices=['Wavelength','Energy'],style=wx.CB_READONLY|wx.CB_DROPDOWN)
        selSizer.Add(self.choice1,0)
        selSizer.Add((10,10),0)
        self.choice1.Bind(wx.EVT_COMBOBOX, self.OnChoice1, id=wxID_CHOICE1)
        mainSizer.Add(selSizer,0)
        mainSizer.Add((10,10),0)

        slideSizer = wx.BoxSizer(wx.HORIZONTAL)
        self.SpinButton = wx.SpinButton(id=wxID_SPINBUTTON, parent=self.panel,
              size=wx.Size(25,24), style=wx.SP_VERTICAL | wx.SP_ARROW_KEYS)
        slideSizer.Add(self.SpinButton,0,wx.ALIGN_RIGHT)
        self.SpinButton.SetRange(-1,1)
        self.SpinButton.SetValue(0)
        self.SpinButton.Bind(wx.EVT_SPIN, self.OnSpinButton, id=wxID_SPINBUTTON)

        self.slider1 = wx.Slider(id=wxID_SLIDER1, maxValue=int(1000.*self.Wmax),
            minValue=int(1000.*self.Wmin), parent=self.panel,style=wx.SL_HORIZONTAL,
            value=int(self.Wave*1000.), )
        slideSizer.Add(self.slider1,1,wx.EXPAND)
        self.slider1.Bind(wx.EVT_SLIDER, self.OnSlider1, id=wxID_SLIDER1)
        mainSizer.Add(slideSizer,0,wx.EXPAND)
        mainSizer.Add((10,10),0)

        cellSizer = wx.BoxSizer(wx.HORIZONTAL)
        cellSizer.Add((5,10),0)
        cellSizer.Add(wx.StaticText(parent=self.panel, label='Volume:'),0,wx.EXPAND)
        cellSizer.Add((5,10),0)
        self.SpinText3 = wx.TextCtrl(id=wxID_SPINTEXT3, parent=self.panel,
              size=wx.Size(100,20), value = "%.2f" % (self.Volume),style=wx.TE_PROCESS_ENTER )
        cellSizer.Add(self.SpinText3,0)
        cellSizer.Add((5,10),0)
        self.SpinText3.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText3, id=wxID_SPINTEXT3)

        cellSizer.Add((5,10),0)
        cellSizer.Add(wx.StaticText(parent=self.panel, label='Z(vol):'),0,wx.EXPAND)
        cellSizer.Add((5,10),0)
        self.SpinText4 = wx.TextCtrl(id=wxID_SPINTEXT4, parent=self.panel,
              size=wx.Size(50,20), value = "%d" % (self.Zcell),style=wx.TE_PROCESS_ENTER )
        cellSizer.Add(self.SpinText4,0)
        cellSizer.Add((5,10),0)
        self.SpinText4.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText4, id=wxID_SPINTEXT4)

        cellSizer.Add((5,10),0)
        cellSizer.Add(wx.StaticText(parent=self.panel, label='Sample R:'),0,wx.EXPAND)
        cellSizer.Add((5,10),0)
        self.SpinText5 = wx.TextCtrl(id=wxID_SPINTEXT5, parent=self.panel,
              size=wx.Size(50,20), value = "%.2f" % (self.Radius),style=wx.TE_PROCESS_ENTER )
        cellSizer.Add(self.SpinText5,0)
        cellSizer.Add((5,10),0)
        self.SpinText5.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText5, id=wxID_SPINTEXT5)

        cellSizer.Add((5,10),0)
        cellSizer.Add(wx.StaticText(parent=self.panel, label='packing:'),0,wx.EXPAND)
        cellSizer.Add((5,10),0)
        self.SpinText6 = wx.TextCtrl(id=wxID_SPINTEXT6, parent=self.panel,
              size=wx.Size(50,20), value = "%.2f" % (self.Pack),style=wx.TE_PROCESS_ENTER )
        cellSizer.Add(self.SpinText6,0)
        cellSizer.Add((5,10),0)
        self.SpinText6.Bind(wx.EVT_TEXT_ENTER, self.OnSpinText6, id=wxID_SPINTEXT6)

        mainSizer.Add(cellSizer,0)
        mainSizer.Add((10,10),0)
        self.panel.SetSizer(mainSizer)
        self.panel.Fit()
        self.panel.GetParent().SetSize()

    def __init__(self, parent):
        self._init_ctrls(parent)
        self.parent = parent
        self.Lines = []
        self.Elems = []
        self.linePicked = None

    def OnExitMenu(self, event):
        self.parent.G2plotNB.Delete('Absorb')
        self.Close()
        self.Destroy()

    def OnNewMenu(self, event):
        ElList = []
        for Elem in self.Elems: ElList.append(Elem[0])
        PE = G2elemGUI.PickElements(self,ElList)
        if PE.ShowModal() == wx.ID_OK:
            Elem = PE.Elem
        PE.Destroy()
        if Elem:
            for El in Elem:
                ElemSym = El.strip().upper()
                if ElemSym not in ElList:
                    atomData = G2elem.GetAtomInfo(ElemSym.capitalize())
                    FormFactors = G2elem.GetFormFactorCoeff(ElemSym)
                    for FormFac in FormFactors:
                        FormSym = FormFac['Symbol'].strip()
                        if FormSym == ElemSym:
                            Z = FormFac['Z']                #At. No.
                            N = 1.                          #no atoms / formula unit
                            Orbs = G2elem.GetXsectionCoeff(ElemSym)
                            Elem = [ElemSym,Z,N,FormFac,Orbs,atomData]
                    self.Elems.append(Elem)
            self.Delete.Enable(True)
            self.panel.Destroy()
            self.DrawPanel()
            self.NewFPPlot = True
            self.SetWaveEnergy(self.Wave)

    def OnDeleteMenu(self, event):
        if len(self.Elems):
            ElList = []
            for Elem in self.Elems: ElList.append(Elem[0])
            S = []
            DE = G2elemGUI.DeleteElement(self,ElList)
            if DE.ShowModal() == wx.ID_OK:
                El = DE.GetDeleteElement().strip().upper()
                for Elem in self.Elems:
                    if Elem[0] != El:
                        S.append(Elem)
                self.Elems = S
                self.CalcFPPS()
                if not self.Elems:
                    self.Delete.Enable(False)
                self.panel.Destroy()
                self.DrawPanel()
                self.NewFPPlot = True
                self.SetWaveEnergy(self.Wave)

    def OnNumElem(self, event):
        for Elem in self.Elems:
            if event.GetEventObject().GetName() == Elem[0]:
                Elem[2] = float(event.GetEventObject().GetValue())
                event.GetEventObject().SetValue("%8.2f" % (Elem[2]))
                self.SetWaveEnergy(self.Wave)

    def OnSpinText1(self, event):
        self.SetWaveEnergy(float(self.SpinText1.GetValue()))

    def OnSpinText2(self, event):
        self.SetWaveEnergy(self.Kev/(float(self.SpinText2.GetValue())))

    def OnSpinText3(self,event):
        self.Volume = max(10.,float(self.SpinText3.GetValue()))
        self.ifVol = True
        self.SetWaveEnergy(self.Wave)

    def OnSpinText4(self,event):
        self.Zcell = max(1,float(self.SpinText4.GetValue()))
        self.SetWaveEnergy(self.Wave)

    def OnSpinText5(self, event):
        self.Radius = max(0.01,float(self.SpinText5.GetValue()))
        self.SetWaveEnergy(self.Wave)

    def OnSpinText6(self, event):
        self.Pack = min(1.0,max(0.01,float(self.SpinText6.GetValue())))
        self.SetWaveEnergy(self.Wave)

    def OnSpinButton(self, event):
        move = self.SpinButton.GetValue()/10000.
        self.Wave = min(max(self.Wave+move,self.Wmin),self.Wmax)
        self.SpinButton.SetValue(0)
        self.SetWaveEnergy(self.Wave)

    def OnSlider1(self, event):
        if self.ifWave:
            Wave = float(self.slider1.GetValue())/1000.
        else:
            Wave = self.Kev/(float(self.slider1.GetValue())/1000.)
        self.SetWaveEnergy(Wave)

    def SetWaveEnergy(self,Wave):
        self.Wave = Wave
        self.Energy = self.Kev/self.Wave
        self.Energy = round(self.Energy,4)
        E = self.Energy
        DE = E*self.Eres                         #smear by defined source resolution
        self.SpinText1.SetValue("%.4f" % (self.Wave))
        self.SpinText2.SetValue("%.4f" % (self.Energy))
        self.SpinText1.Update()
        self.SpinText2.Update()
        if self.ifWave:
            self.slider1.SetValue(int(1000.*self.Wave))
        else:
            self.slider1.SetValue(int(1000.*self.Energy))
        Text = ''
        if not self.ifVol:
            self.Volume = 0
            for Elem in self.Elems:
                self.Volume += 10.*Elem[2]
        muT = 0
        Mass = 0
        Fo = 0
        Fop = 0
        for Elem in self.Elems:
            Mass += self.Zcell*Elem[2]*Elem[5]['Mass']
            r1 = G2elem.FPcalc(Elem[4],E+DE)
            r2 = G2elem.FPcalc(Elem[4],E-DE)
            Els = Elem[0]
            Els = Els.ljust(2).lower().capitalize()
            mu = 0
            Fo += Elem[2]*Elem[1]
            if Elem[1] > 78 and self.Energy+DE > self.Kev/0.16:
                mu = self.Zcell*Elem[2]*(r1[2]+r2[2])/2.0
                Text += "%s\t%s%8.2f  %s%6s  %s%6.3f  %s%10.2f %s\n" %    (
                    'Element= '+str(Els),"N = ",Elem[2]," f'=",'not valid',
                    ' f"=',(r1[1]+r2[1])/2.0,' '+Gkmu+'=',mu,'barns')
            elif Elem[1] > 94 and self.Energy-DE < self.Kev/2.67:
                mu = 0
                Text += "%s\t%s%8.2f  %s%6s  %s%6s  %s%10s%s\n" %    (
                    'Element= '+str(Els),"N = ",Elem[2]," f'=",'not valid',
                    ' f"=','not valid',' '+Gkmu+'=','not valid')
            else:
                mu = self.Zcell*Elem[2]*(r1[2]+r2[2])/2.0
                Fop += Elem[2]*(Elem[1]+(r1[0]+r2[0])/2.0)
                Text += "%s\t%s%8.2f  %s%6.3f  %s%6.3f  %s%10.2f %s\n" %    (
                    'Element= '+str(Els),"N = ",Elem[2]," f'=",(r1[0]+r2[0])/2.0,
                    ' f"=',(r1[1]+r2[1])/2.0,' '+Gkmu+'=',mu,'barns')
            muT += mu

        if self.Volume:
            Text += "%s %s%10.2f %s" % ("Total",' '+Gkmu+'=',self.Pack*muT/self.Volume,'cm'+Pwrm1+', ')
            Text += "%s%10.2f%s" % ('Total '+Gkmu+'R=',self.Radius*self.Pack*muT/(10.0*self.Volume),', ')
            Text += "%s%10.4f%s\n" % ('Transmission exp(-2'+Gkmu+'R)=', \
                100.0*math.exp(-2*self.Radius*self.Pack*muT/(10.0*self.Volume)),'%')
            self.Results.SetValue(Text)
            den = Mass/(0.602*self.Volume)
            if self.ifVol:
                Text += '%s' % ('Theor. density=')
            else:
                Text += '%s' % ('Est. density=')
            Text += '%6.3f %s%.3f %s\n' % (den,'g/cm'+Pwr3+', Powder density=',self.Pack*den,'g/cm'+Pwr3)
            Text += '%s%10.2f%s\n'%('X-ray small angle scattering contrast',(28.179*Fo/self.Volume)**2,'*10'+Pwr20+'/cm'+Pwr4)
            if Fop:
                Text += '%s%10.2f%s\n'%('Anomalous X-ray small angle scattering contrast',(28.179*Fop/self.Volume)**2,'*10'+Pwr20+'/cm'+Pwr4)
            self.Results.SetValue(Text)
        self.Results.Update()
        self.SpinText3.SetValue("%.2f" % (self.Volume))
        self.SpinText3.Update()
        self.SpinText4.SetValue("%d" % (self.Zcell))
        self.SpinText4.Update()
        self.SpinText5.SetValue("%.2f" % (self.Radius))
        self.SpinText5.Update()
        self.SpinText6.SetValue("%.2f" % (self.Pack))
        self.SpinText6.Update()
        if len(self.Elems):
            self.CalcFPPS()
            self.UpDateAbsPlot(Wave,rePlot=True)

    def CalcFPPS(self):
        """generate f" curves for selected elements
           does constant delta-lambda/lambda steps over defined range
        """
        FPPS = []
        if self.Elems:
            wx.BeginBusyCursor()
            Corr = self.Zcell*self.Radius*self.Pack/(10.0*self.Volume)
            try:
                muT = []
                for iE,Elem in enumerate(self.Elems):
                    Els = Elem[0]
                    Els = Els = Els.ljust(2).lower().capitalize()
                    Wmin = self.Wmin
                    Wmax = self.Wmax
                    lWmin = math.log(Wmin)
                    N = int(round(math.log(Wmax/Wmin)/self.Wres))    #number of constant delta-lam/lam steps
                    I = range(N+1)
                    Ws = []
                    for i in I: Ws.append(math.exp(i*self.Wres+lWmin))
                    mus = []
                    Es = []
                    for j,W in enumerate(Ws):
                        E = self.Kev/W
                        DE = E*self.Eres                         #smear by defined source resolution
                        res1 = G2elem.FPcalc(Elem[4],E+DE)
                        res2 = G2elem.FPcalc(Elem[4],E-DE)
                        muR = Corr*Elem[2]*(res1[2]+res2[2])/2.0
                        mus.append(muR)
                        if iE:
                            muT[j] += muR
                        else:
                            muT.append(muR)
                        Es.append(E)
                    if self.ifWave:
                        Fpps = (Els,Ws,mus)
                    else:
                        Fpps = (Els,Es,mus)
                    FPPS.append(Fpps)
                if self.ifWave:
                    Fpps = ('Total',Ws,muT)
                else:
                    Fpps = ('Total',Es,muT)
                FPPS.append(Fpps)
            finally:
                wx.EndBusyCursor()
        self.FPPS = FPPS

    def OnChoice1(self, event):
        if event.GetString() == "Wavelength":
            self.ifWave = True
            self.NewFPPlot = True
            self.Wave = round(self.Wave,4)
            self.slider1.SetRange(int(1000.*self.Wmin),int(1000.*self.Wmax))
            self.slider1.SetValue(int(1000.*self.Wave))
            self.SpinText1.SetValue("%6.4f" % (self.Wave))
            self.SpinText2.SetValue("%7.4f" % (self.Energy))
        else:
            self.ifWave = False
            self.NewFPPlot = True
            Emin = self.Kev/self.Wmax
            Emax = self.Kev/self.Wmin
            self.Energy = round(self.Energy,4)
            self.slider1.SetRange(int(1000.*Emin),int(1000.*Emax))
            self.slider1.SetValue(int(1000.*self.Energy))
            self.SpinText1.SetValue("%6.4f" % (self.Wave))
            self.SpinText2.SetValue("%7.4f" % (self.Energy))
        if len(self.Elems):
            self.CalcFPPS()
            self.UpDateAbsPlot(self.Wave,rePlot=False)

    def OnKeyPress(self,event):
        if event.key == 'g':
            mpl.rcParams['axes.grid'] = not mpl.rcParams['axes.grid']
            self.UpDateAbsPlot(self.Wave,rePlot=False)

    def UpDateAbsPlot(self,Wave,rePlot=True):
        """Plot mu vs wavelength 0.05-3.0A"""
        xylim = []
        try:
            if rePlot:
                asb = self.Page.figure.get_axes()[1]
                xylim = asb.get_xlim(),asb.get_ylim()
            newPlot = False
        except:
            new,plotNum,self.Page,self.fplot,lim = self.parent.G2plotNB.FindPlotTab('Absorb','mpl')
            self.Page.canvas.mpl_connect('pick_event', self.OnPick)
            self.Page.canvas.mpl_connect('button_release_event', self.OnRelease)
            self.Page.canvas.mpl_connect('motion_notify_event', self.OnMotion)
            self.Page.canvas.mpl_connect('key_press_event', self.OnKeyPress)
            newPlot = True
            self.ax = self.Page.figure.add_subplot(111,label='absorb')
        self.fplot.set_visible(False)
        self.Page.Choice = (' key press','g: toggle grid',)
        self.Page.keyPress = self.OnKeyPress
        self.ax.clear()
        self.ax.set_title('X-Ray Absorption',x=0,ha='left')
        self.ax.set_ylabel(r"$\mu R$",fontsize=14)
        Ymin = 0.0
        Ymax = 0.0
        if self.FPPS:
            for Fpps in self.FPPS:
                Ymin = min(Ymin,min(Fpps[2]))
                Ymax = max(Ymax,max(Fpps[2]))
                fppsP1 = np.array(Fpps[1])
                fppsP2 = np.array(Fpps[2])
                self.ax.plot(fppsP1,fppsP2,label=r'$\mu R$ '+Fpps[0])
        if self.ifWave:
            self.ax.set_xlabel(r'$\mathsf{\lambda, \AA}$',fontsize=14)
            self.ax.axvline(x=Wave,picker=3,color='black')
        else:
            self.ax.set_xlabel(r'$\mathsf{E, keV}$',fontsize=14)
            self.ax.set_xscale('log')
            self.ax.axvline(x=self.Kev/Wave,picker=3,color='black')
        self.ax.axhline(y=1.0,color='b')
        self.ax.axhline(y=5.0,color='r')
        self.ax.set_ylim(Ymin,Ymax)
        if self.FPPS:
            self.ax.legend(loc='best')
        if newPlot:
            newPlot = False
            self.Page.canvas.draw()
        else:
            if rePlot:
                tb = self.Page.canvas.toolbar
                tb.push_current()
                self.ax.set_xlim(xylim[0])
                self.ax.set_ylim(xylim[1])
                xylim = []
                tb.push_current()
            self.Page.canvas.draw()

    def OnPick(self, event):
        self.linePicked = event.artist

    def OnMotion(self,event):
        xpos = event.xdata
        if xpos and xpos>0.1:
            ypos = event.ydata
            if self.ifWave:
                Wave = xpos
            else:
                Wave = self.Kev/xpos
            Wave = min(max(Wave,self.Wmin),self.Wmax)
            self.parent.G2plotNB.status.SetStatusText('Wavelength: %.4f, Energy: %.3f, %sR: %.3f'%(Wave,self.Kev/Wave,Gkmu,ypos),1)
        if self.linePicked:
            self.SetWaveEnergy(Wave)

    def OnRelease(self, event):
        if self.linePicked is None: return
        self.linePicked = None
        xpos = event.xdata
        if xpos:
            if self.ifWave:
                Wave = xpos
            else:
                Wave = self.Kev/xpos
            self.SetWaveEnergy(Wave)

    def OnABOUTItems0Menu(self, event):
        ''' '''
        try:
            import wx.adv as wxadv  # AboutBox moved here in Phoenix
        except:
            wxadv = wx
        info = wxadv.AboutDialogInfo()
        info.Name = 'Absorb'
        info.Copyright = '''
Robert B. Von Dreele, 2009(C)
Argonne National Laboratory
This product includes software developed
by the UChicago Argonne, LLC, as
Operator of Argonne National Laboratory.        '''
        info.Description = '''
For calculating X-ray absorption factors to 250keV for cylindrical
powder samples; based on Fortran program Fprime of Cromer & Liberman
(D. T. Cromer and D. A. Liberman, Acta Cryst. (1981). A37, 267-268.)
corrected for Kissel & Pratt energy term; Jensen term not included
        '''
        wxadv.AboutBox(info)


"""
BEACON
"""

import sys, os
import numpy as np
import libraries as lib

def parameters_generator(Filename='Options'):
    import imp

    options = imp.load_source(os.path.splitext(os.path.basename(Filename))[0], Filename)

    class galaxy:
        def __init__(self):
            self.Name = options.Galaxy_Name
            self.Sky_Coo = np.array(options.Galaxy_Coordinates)
            self.Distance =  options.Galaxy_Distance
            self.RH = options.Galaxy_rh
            self.RT = options.Galaxy_rt
            self.PA = options.Galaxy_pa
            self.E =  options.Galaxy_e
            self.Sky_Vel =  options.Galaxy_Velocity

    class read_data_options:
        def __init__(self):
            self.Data_file = options.Galaxy_Data_file
            self.Clean_sample = options.Clean_sample
            self.Admisible_range_Vel = options.Admisible_range_Velocity
            self.Admisible_max_error_Vel = options.Admisible_max_error_Velocity
            self.Admisible_max_error_Extra_Coo = options.Admisible_max_error_Extra_Coo

    class beacon_options:
        def __init__(self):
            self.Smoothing = options.Smoothing
            self.Min_cluster_size = options.Min_cluster_size
            self.Maxima_ratio = options.Maxima_ratio
            self.Epsilon = options.Epsilon
            self.Standarization_Method = options.Standarization_Method
            self.Standarization_Weights = np.array(options.Standarization_Weights)
            self.Coordinate_system = options.Coordinate_system
            self.Velocity_system = options.Velocity_system
            self.Uniqueness_Method = options.Uniqueness_Method
            self.NStars_normed = options.NStars_normed

    class sref_options:
        def __init__(self):
            self.WCS = options.Galaxy_WCS
            self.Extra_Coo_is_FeH = options.Extra_Coo_is_FeH
            self.Use_elliptical_coordinates = options.Use_elliptical_coordinates
            self.Find_centre_sky_radius = options.Find_centre_sky_radius
            self.Find_centre_sky_resolution = options.Find_centre_sky_resolution
            self.Find_centre_vhel_radius = options.Find_centre_vhel_radius
            self.Find_centre_vhel_resolution = options.Find_centre_vhel_resolution

    class plotting_options:
        def __init__(self):
            self.Plot_maps = options.Plot_maps
            self.wcs = options.Galaxy_WCS
            self.Maps_plotting_radius = options.Maps_plotting_radius
            self.Maps_vlos_limits = options.Maps_vlos_limits
            self.Auxiliar_axes_limits = options.Auxiliar_axes_limits
            self.Draw_markers = options.Draw_markers
            self.Markers_color = options.Markers_color
            self.Markers_size = options.Markers_size
            self.Markers_linewidth = options.Markers_linewidth
            self.Plot_angular_momenta = options.Plot_angular_momenta
            self.Plot_extra_coo = options.Plot_extra_coo
            self.Plot_elliptical_radius = options.Use_elliptical_coordinates
            if options.Auxiliar_axes_label:
               self.Auxiliar_axes_label = options.Auxiliar_axes_label

    return galaxy(), read_data_options(), beacon_options(), sref_options(), plotting_options()

def stars_generator(Rest_frame_coo):
    class stars_data(object):
        def __init__(self):
            self.Sky_Coo = Rest_frame_coo[0]
            self.Gref_car_Coo = Rest_frame_coo[1]
            self.Gref_car_Vel = Rest_frame_coo[2]
            self.Gref_car_eVel = Rest_frame_coo[3]
            self.Gref_pol_Coo = Rest_frame_coo[4]
            self.Gref_pol_Vel = Rest_frame_coo[5]
            self.Gref_pol_eVel = Rest_frame_coo[6]
            try:
               self.Z = Rest_frame_coo[7]
               self.eZ = Rest_frame_coo[8]
               self.FeH = Rest_frame_coo[9]
               self.eFeH = Rest_frame_coo[10]
            except:
               pass
            try:
               self.Extra_Coo = Rest_frame_coo[7]
               self.eExtra_Coo = Rest_frame_coo[8]
            except:
               pass

    return stars_data

def read_data(read_data_options):

    Stars_data = np.loadtxt(read_data_options.Data_file, skiprows=1)

    try:
       Sky_Coo = Stars_data[:,0:2]
       Sky_Vel = Stars_data[:,2:7:2]
       Sky_eVel = Stars_data[:,3:8:2]
       Extra_Coo = Stars_data[:,-2:7:-2]
       Extra_eCoo = Stars_data[:,-1:7:-2]
    except:
       print 'Bad format in input table.\n'
       print 'Format shoud be:\n'
       print 'RA Dec VRA eVRA VDec eVDec Vlos eVlos Extra_Coo eExtra_Coo Extra_Coo_2 eExtra_Coo_2 ...'
       print 'Quitting now...'
       sys.exit(0)

    Sky_eVel = np.where(Sky_eVel < 1e-5, np.average(Sky_eVel), Sky_eVel)
    Extra_eCoo = np.where(Extra_eCoo < 1e-5, np.average(Extra_eCoo), Extra_eCoo)

    Std_stars_data = [Sky_Coo, Sky_Vel, Sky_eVel, Extra_Coo, Extra_eCoo]

    return Std_stars_data

def ref_frame(Std_data, galaxy, sref_options):

    """
    Sky, WCS and physical coordinates
    """
    if sref_options.WCS:
       Sky_Coo = Std_data[0]
       Gref_car_Coo = lib.wcs2xy(Sky_Coo, galaxy.Sky_Coo, galaxy.Distance)

    else:
       Gref_car_Coo = Std_data[0]
       Sky_Coo = lib.xy2wcs(Gref_car_Coo, galaxy.Sky_Coo, galaxy.Distance)

    """
    Galactic velocity rest frame
    """
    Gref_car_Vel = Std_data[1] - galaxy.Sky_Vel
    Gref_car_eVel = Std_data[2]

    if sref_options.Use_elliptical_coordinates:
       Gref_car_Coo = lib.deproject_ellip(Gref_car_Coo, Ellipticity = galaxy.E, PA = galaxy.PA)
       try:
          Gref_car_Vel[:,0:2] = lib.deproject_ellip(Gref_car_Vel[:,0:2], Ellipticity = galaxy.E, PA = galaxy.PA)
          Gref_car_eVel[:,0:2] = lib.deproject_ellip(Gref_car_eVel[:,0:2], Ellipticity = galaxy.E, PA = galaxy.PA)
       except:
          pass
    """
    Galactic elliptical coordinates
    """
    Gref_pol_Coo = lib.polar_coo(Gref_car_Coo)
    Gref_pol_Vel = lib.polar_vel(Gref_car_Coo, Gref_car_Vel)
    Gref_pol_eVel = np.abs(lib.polar_vel(Gref_car_Coo, Gref_car_eVel))

    print 'Right now beacon accepts either FeH and eFeH, or whatever linear coordinates user may provide, but NOT both (FeH and linear coordinates) at the same time!'
    try:
       Extra_Coo, Extra_eCoo = Std_data[3], Std_data[4]
       if sref_options.Extra_Coo_is_FeH:
          Z, eZ = lib.log2lin_metal(Extra_Coo, Extra_eCoo)
          FeH, eFeH = Extra_Coo, Extra_eCoo
          Rest_frame_coo = [Sky_Coo, Gref_car_Coo, Gref_car_Vel, Gref_car_eVel, Gref_pol_Coo, Gref_pol_Vel, Gref_pol_eVel, Z, eZ, FeH, eFeH]
       else:
          Rest_frame_coo = [Sky_Coo, Gref_car_Coo, Gref_car_Vel, Gref_car_eVel, Gref_pol_Coo, Gref_pol_Vel, Gref_pol_eVel, Extra_Coo, Extra_eCoo]
    except:
       Rest_frame_coo = [Sky_Coo, Gref_car_Coo, Gref_car_Vel, Gref_car_eVel, Gref_pol_Coo, Gref_pol_Vel, Gref_pol_eVel]

    Stars_data_rf = stars_generator(Rest_frame_coo)

    return Stars_data_rf()

def rest_frame_generator(galaxy, read_data_options, sref_options):

    if (sref_options.Find_centre_sky_radius != 0) and (sref_options.Find_centre_vhel_radius !=0):
        Find_centre_coordinates_list = np.mgrid[galaxy.Sky_Coo[0]-sref_options.Find_centre_sky_radius/60. : galaxy.Sky_Coo[0]+sref_options.Find_centre_sky_radius/60. :  sref_options.Find_centre_sky_resolution/60., galaxy.Sky_Coo[1]-sref_options.Find_centre_sky_radius/60. :  galaxy.Sky_Coo[1]+sref_options.Find_centre_sky_radius/60. : sref_options.Find_centre_sky_resolution/60., galaxy.Sky_Vel-sref_options.Find_centre_vhel_radius : galaxy.Sky_Vel+sref_options.Find_centre_vhel_radius: sref_options.Find_centre_vhel_resolution]

        Find_centre_RA_sky_coordinates_List = Find_centre_coordinates_list[0].flatten()
        Find_centre_Dec_sky_coordinates_List = Find_centre_coordinates_list[1].flatten()
        Find_centre_Vhel_sky_coordinates_List = Find_centre_coordinates_list[2].flatten()
        Dimensions = np.shape(Find_centre_coordinates_list[0])

    elif (sref_options.Find_centre_sky_radius != 0) and (sref_options.Find_centre_vhel_radius == 0):
        Sky_coo_list = np.mgrid[galaxy.Sky_Coo[0]-sref_options.Find_centre_sky_radius/60. : galaxy.Sky_Coo[0]+sref_options.Find_centre_sky_radius/60. :  sref_options.Find_centre_sky_resolution/60., galaxy.Sky_Coo[1]-sref_options.Find_centre_sky_radius/60. :  galaxy.Sky_Coo[1]+sref_options.Find_centre_sky_radius/60. : sref_options.Find_centre_sky_resolution/60.]

        Find_centre_RA_sky_coordinates_List = Sky_coo_list[0].flatten()
        Find_centre_Dec_sky_coordinates_List = Sky_coo_list[1].flatten()
        Find_centre_Vhel_sky_coordinates_List = np.ones(len(Sky_coo_list[0].flatten()))*galaxy.Sky_Vel
        Dimensions = np.shape(Sky_coo_list[0])

    elif (sref_options.Find_centre_sky_radius == 0) and (sref_options.Find_centre_vhel_radius != 0):
        Find_centre_RA_sky_coordinates_List = np.ones(len(Vhel_coo_list))*galaxy.Sky_Coo[0]
        Find_centre_Dec_sky_coordinates_List = np.ones(len(Vhel_coo_list))*galaxy.Sky_Coo[1]
        Find_centre_Vhel_sky_coordinates_List = np.arange(galaxy.Sky_Vel-sref_options.Find_centre_vhel_radius, galaxy.Sky_Vel+sref_options.Find_centre_vhel_radius, sref_options.Find_centre_vhel_resolution)
        Dimensions = 1

    else:
        Find_centre_RA_sky_coordinates_List = [galaxy.Sky_Coo[0]]
        Find_centre_Dec_sky_coordinates_List = [galaxy.Sky_Coo[1]]
        Find_centre_Vhel_sky_coordinates_List = [galaxy.Sky_Vel]
        Dimensions = 0

    Std_data = read_data(read_data_options)

    Stars_data_lis = []

    for Find_centre_RA_sky_coordinates, Find_centre_Dec_sky_coordinates, Find_centre_Vhel_coordinates in zip(Find_centre_RA_sky_coordinates_List, Find_centre_Dec_sky_coordinates_List, Find_centre_Vhel_sky_coordinates_List):

       galaxy.Sky_Coo = np.array([Find_centre_RA_sky_coordinates, Find_centre_Dec_sky_coordinates])
       galaxy.Sky_Vel = np.array([Find_centre_Vhel_coordinates])

       Stars_data_lis.append(ref_frame(Std_data, galaxy, sref_options))

    return Stars_data_lis, Find_centre_RA_sky_coordinates_List, Find_centre_Dec_sky_coordinates_List, Find_centre_Vhel_sky_coordinates_List, Dimensions

def autoorder(Possible_Circular_Stream_Indices, Possible_NonCircular_Stream_Indices, stars_data):

    try:
       OV = stars_data.Z
    except:
       OV = stars_data.Extra_Coo

    Possible_Circular_Stream_Mean_OV = []
    for Circ_Index in Possible_Circular_Stream_Indices:
        Possible_Circular_Stream_Mean_OV.append(np.mean(OV[list(Circ_Index)]))

    Possible_NonCircular_Stream_Mean_OV = []
    for NonCirc_Index in Possible_NonCircular_Stream_Indices:
        Possible_NonCircular_Stream_Mean_OV.append(np.mean(OV[list(NonCirc_Index)]))

    """
    Order Results By OV
    """
    Possible_Circular_Stream_Sorted_Indices = sorted(range(len(Possible_Circular_Stream_Mean_OV)), key=lambda k: Possible_Circular_Stream_Mean_OV[k])
    Possible_NonCircular_Stream_Sorted_Indices = sorted(range(len(Possible_NonCircular_Stream_Mean_OV)), key=lambda k: Possible_NonCircular_Stream_Mean_OV[k])

    Possible_Circular_Stream_Indices = [Possible_Circular_Stream_Indices[i] for i in Possible_Circular_Stream_Sorted_Indices]

    Possible_NonCircular_Stream_Indices = [Possible_NonCircular_Stream_Indices[i] for i in Possible_NonCircular_Stream_Sorted_Indices]

    return Possible_Circular_Stream_Indices, Possible_NonCircular_Stream_Indices

def beacon(Rest_frame_coo, beacon_options):

    """
    This routine will call OPTICS with an apropiate set of coordinates.
    Tipically coordinates, velocities, and chemical abundances
    """

    if (beacon_options.Coordinate_system == 'pol'):
       """
       Coo are the coordinates of the stars. The angle (Theta), must be inverted, but not the radius
       """
       Coo = Rest_frame_coo.Gref_pol_Coo
       Simmet_Coo = np.array([Rest_frame_coo.Gref_pol_Coo[:,0], (Rest_frame_coo.Gref_pol_Coo[:,1]+np.pi) % (2.*np.pi)]).T

    if (beacon_options.Velocity_system == 'pol'):
       """
       Polar velocities are expected to be the same at both sides of
       the CM, except for the line-of-sight (z) component, that shoud be inverted.
       """
       Vel = Rest_frame_coo.Gref_pol_Vel
       Simmet_Vel = np.array([Vel[:,0], Vel[:,1], -1.*Vel[:,2]]).T

    if (beacon_options.Coordinate_system == 'car'):
       """
       If one prefers cartesian coordinates, then both have to be inverted
       """
       Coo = Rest_frame_coo.Gref_car_Coo
       Simmet_Coo = np.array([(-Rest_frame_coo.Gref_car_Coo[:,0]), -Rest_frame_coo.Gref_car_Coo[:,1]]).T

    if (beacon_options.Velocity_system == 'car'):
       """
       All cartesian components of the velocity are expected to be the
       oposite at the other side of the CM.
       """
       Vel = Rest_frame_coo.Gref_car_Vel
       Simmet_Vel = -1.*Vel

    """
    Extra properties should be consistently equal at both sides of the CM (In case they exist)
    """
    try:
       Extra = Rest_frame_coo.Z
    except:
       pass
    try:
       Extra = Rest_frame_coo.Extra_Coo
    except:
       pass
    try:
       if Extra.ndim == 1:
          Extra = np.expand_dims(Extra, axis=1)

       Direct_Clustering_Data = np.hstack([Coo, Vel, Extra])
       Simmet_Clustering_Data = np.hstack([Simmet_Coo, Simmet_Vel, Extra])
    except:
       Direct_Clustering_Data = np.hstack([Coo, Vel])
       Simmet_Clustering_Data = np.hstack([Simmet_Coo, Simmet_Vel])

    OAL = len(Direct_Clustering_Data)   # Original Array Length

    """
    If the only coordinates being used are v_rot and v_rad then don't use the simmetric vector
    """
    if (beacon_options.Velocity_system == 'pol') & (beacon_options.Standarization_Weights[4] == 0):
      Clustering_Data = Direct_Clustering_Data
      mcs = beacon_options.Min_cluster_size
    else:
      Clustering_Data = np.vstack([Direct_Clustering_Data, Simmet_Clustering_Data])
      mcs = float(beacon_options.Min_cluster_size)*2.

    if not Clustering_Data.size:
      print 'No stars remain after cleaning, check option file!'
      quit()
    """
    Standardization
    """
    if beacon_options.Standarization_Method == 'ptp':
        Clustering_Data /= np.ptp(Direct_Clustering_Data, axis=0)
    elif beacon_options.Standarization_Method == 'std':
        Clustering_Data /= np.std(Direct_Clustering_Data, axis=0)
    elif beacon_options.Standarization_Method == 'var':
        Clustering_Data /= np.var(Direct_Clustering_Data, axis=0)

    else:
        print "Non standarization was applied!"

    try:
        Clustering_Data *= beacon_options.Standarization_Weights
    except:
        print "Incorrect dimension of weights. Same weight applied to all coordinates."

    Clustering_Data = Clustering_Data[:,np.where(beacon_options.Standarization_Weights != 0)[0]]

    """
    OPTICS call
    """
    Indices = optics_launcher(Clustering_Data, beacon_options.Smoothing, min_cluster_size = mcs, maxima_ratio = beacon_options.Maxima_ratio)

    Possible_Circular_Stream_Indices = []
    Possible_NonCircular_Stream_Indices = []
    for ii, Index in enumerate(Indices):
        if (sum(1 for i in Index if i >= OAL) > len(Index)/beacon_options.Epsilon) & (sum(1 for i in Index if i < OAL) > len(Index)/beacon_options.Epsilon):
            Possible_Circular_Stream = True
            Possible_Nonirc_streams = False
        elif ((Index < OAL).all()) or ((Index >= OAL).all()):
            Possible_Circular_Stream = False
            Possible_Nonirc_streams = True
        else:
            Possible_Circular_Stream = False
            Possible_Nonirc_streams = False
        Index[Index >= OAL] -= OAL
        if Possible_Circular_Stream:
            Possible_Circular_Stream_Indices.append(set(Index))
        elif Possible_Nonirc_streams:
            Possible_NonCircular_Stream_Indices.append(set(Index))

    """
    Uniqueness of the groups
    """
    if beacon_options.Uniqueness_Method == 'all':
        Possible_Circular_Stream_Indices = [list(i) for i in set(tuple(i) for i in Possible_Circular_Stream_Indices)]
        Possible_NonCircular_Stream_Indices = [list(i) for i in set(tuple(i) for i in Possible_NonCircular_Stream_Indices)]

    if beacon_options.Uniqueness_Method == 'any':
        from networkx.algorithms.components.connected import connected_components
        Possible_Circular_Stream_Indices = list(connected_components(lib.to_graph(Possible_Circular_Stream_Indices)))
        Possible_NonCircular_Stream_Indices = list(connected_components(lib.to_graph(Possible_NonCircular_Stream_Indices)))

    """
    If there are many groups, exit program
    """
    if (len(Possible_Circular_Stream_Indices) > 1000) or (len(Possible_NonCircular_Stream_Indices) > 1000):
        print 'To many groups: ',len(Possible_Circular_Stream_Indices),' aborting to prevent computer failure...'
        sys.exit(0)

    return Possible_Circular_Stream_Indices, Possible_NonCircular_Stream_Indices

def useful_quantities(stars_data, galaxy, sref_options, Indices_list=None):
    """
    This routine calculates some useful quantities for plotting and arranging results
    """
    if Indices_list is None:
        Indices_list = [list(np.arange(len(stars_data.Gref_car_Coo)))]

    """
    Averages must be derived using linear quantities
    """
    N_Stars = []
    Velocities_stars = []
    Extra_Coo_stars = []
    Radius_stars = []

    Velocity_groups = []
    Extra_Coo_groups = []
    Radius_groups = []
    Angular_Momenta_groups = []

    print 'WARNING: Projected angular momentum (line-of-sight velocities) with no error!'

    for ii, Indices in enumerate(Indices_list):
        Indices = list(Indices)
        """
        Angular Momentum
        """
        L_k = lib.kinetic_pa(stars_data.Gref_car_Coo[Indices, :], stars_data.Gref_car_Vel[Indices, 2], 1./stars_data.Gref_car_eVel[Indices, 2])

        kl = lib.kinetic_pa(stars_data.Gref_car_Coo[Indices, :], stars_data.Gref_car_Vel[Indices, 2], 1./stars_data.Gref_car_eVel[Indices, 2])
        if sref_options.Use_elliptical_coordinates:
           kl[0:2] =  lib.deproject_ellip(kl[0:2], Ellipticity = 1./(1.-1./galaxy.E), PA = galaxy.PA)

        Angular_Momenta_groups.append(kl)

        try:
           Extra_Coo_stars.append([stars_data.FeH[Indices], stars_data.eFeH[Indices]])

           Avg_Std_Z = lib.avg_std(stars_data.Z[Indices], 1./(stars_data.eZ[Indices])**2)

           Extra_Coo_groups.append(lib.lin2log_metal(Avg_Std_Z[0], Avg_Std_Z[1]))
        except:
           pass
        try:
           Extra_Coo_stars.append([stars_data.Extra_Coo[Indices], stars_data.Extra_eCoo[Indices]])

           Extra_Coo_groups.append(lib.avg_std(stars_data.Extra_Coo[Indices], 1./(stars_data.Extra_eCoo[Indices])**2))
        except:
           pass

        N_Stars.append(len(Indices))
        Velocities_stars.append([stars_data.Gref_car_Vel[Indices],stars_data.Gref_car_eVel[Indices]])

        Radius_stars.append(np.sqrt(np.sum(stars_data.Gref_car_Coo[Indices, :]**2, axis = 1)))

        Velocity_groups.append(lib.avg_std((stars_data.Gref_car_Coo[Indices]), 1./(stars_data.Gref_car_Coo[Indices])**2))

        Radius_groups.append(np.average(np.sqrt(np.sum(stars_data.Gref_car_Coo[Indices, :]**2, axis = 1))))

    """
    CONVERTING LISTS TO ARRAYS
    """
    N_Stars = np.array(N_Stars)
    Angular_Momenta_groups = np.array(Angular_Momenta_groups)

    Radius_stars = np.array(Radius_stars)
    Velocity_groups = np.array(Velocity_groups)

    """
    These quantities may be optional
    """
    Extra_Coo_stars = np.array(Extra_Coo_stars)
    Extra_Coo_groups =  np.array(Extra_Coo_groups)

    Mean_quantities = [Extra_Coo_groups, Velocity_groups, Radius_groups, Angular_Momenta_groups]
    Raw_quantities = [N_Stars, Extra_Coo_stars, Velocities_stars, Radius_stars]

    return Raw_quantities, Mean_quantities

def save_clusters(Circ_mean_quantities, File_name = 'Output/Clusters.dat'):
    Circ_Mean_Metallicity, Circ_Mean_Velocity, Circ_Mean_rDistance, Circ_KL = Circ_mean_quantities

    np.savetxt(File_name, np.array([np.arange(len(Circ_Mean_Metallicity))+1, Circ_Mean_Metallicity[:,0], Circ_Mean_Metallicity[:,1], Circ_KL[:,0], Circ_KL[:,1]]).T, fmt='%i  %.3f  %.3f  %.3f  %.3f', header='Pop   Metal   eMetal   L_angle  L_modulus')

def save_clustered_stars(stars_data, Stream_Indices, File_name = 'Output/Stars.dat'):
   Circ_clustered_stars = []
   for kk, Indices  in enumerate(Stream_Indices):
      Indices = list(Indices)

      Cluster = np.array([np.array(Indices)+1., stars_data.Sky_Coo[Indices,0], stars_data.Sky_Coo[Indices,1], stars_data.Gref_car_Vel[Indices,2], stars_data.Gref_car_eVel[Indices,2], np.squeeze(stars_data.FeH[Indices]), np.squeeze(stars_data.eFeH[Indices]), np.ones(len(stars_data.FeH[Indices]))*(kk+1)]).T

      Circ_clustered_stars.append(Cluster)
   Circ_clustered_stars = np.array([item for sublist in Circ_clustered_stars for item in sublist])

   np.savetxt(File_name, Circ_clustered_stars, fmt='%i  %.8f  %.8f  %.3f  %.3f  %.3f  %.3f  %i', header='Indices    RA    DEC    V_los    eV_los    Metal    eMetal    Pop')

def optics_launcher(Data, Smoothing, min_cluster_size = 5., maxima_ratio = 0.75):
    import optics_core as core

    RD, CD, Order = core.optics(Data,Smoothing)

    Reach_Plot = []
    Reach_Points = []

    for item in Order:
        Reach_Plot.append(RD[item])
        Reach_Points.append(Data[item])

    min_cluster_size_ratio = min_cluster_size / float(len(Data))

    rootNode = core.auto_cluster(Reach_Plot, Reach_Points, min_cluster_size_ratio, maxima_ratio)

    #get only the leaves of the tree
    leaves = core.get_leaves(rootNode, [])

    index_ordered = []
    for item in leaves:
        index_ordered.append(np.array(Order[item.start:item.end]))
    return index_ordered

"""
Andres del Pino Molina
"""


import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

from .module import *

Align_Corners_Range = False

class DepthNet(nn.Module):
    def __init__(self, mode="regression"):
        super(DepthNet, self).__init__()
        self.mode = mode
        assert self.mode in ("regression", "classification", "unification"), "Don't support {}!".format(mode)

    def forward(self, cost_reg, depth_values, num_depth, interval, prob_volume_init=None):
        prob_volume_pre = cost_reg.squeeze(1)  # (b, d, h, w)

        if prob_volume_init is not None:
            prob_volume_pre += prob_volume_init

        if self.mode == "regression":
            prob_volume = F.softmax(prob_volume_pre, dim=1)  # (b, ndepth, h, w)
            depth = depth_regression(prob_volume, depth_values=depth_values)  # (b, h, w)
            with torch.no_grad():
                # photometric confidence
                prob_volume_sum4 = 4 * F.avg_pool3d(F.pad(prob_volume.unsqueeze(1), pad=(0, 0, 0, 0, 1, 2)), (4, 1, 1), stride=1,
                                                    padding=0).squeeze(1)
                depth_index = depth_regression(prob_volume,
                                               depth_values=torch.arange(num_depth, device=prob_volume.device, dtype=torch.float)).long()
                depth_index = depth_index.clamp(min=0, max=num_depth - 1)
                photometric_confidence = torch.gather(prob_volume_sum4, 1, depth_index.unsqueeze(1)).squeeze(1)
        elif self.mode == "classification":
            prob_volume = F.softmax(prob_volume_pre, dim=1)  # (b, ndepth, h, w)
            depth = winner_take_all(prob_volume, depth_values)  # (b, h, w)
            photometric_confidence, _ = torch.max(prob_volume, dim=1)
        elif self.mode == "unification":
            prob_volume = torch.sigmoid(prob_volume_pre)  # (b, ndepth, h, w)
            depth = unity_regression(prob_volume, depth_values, interval)
            photometric_confidence, _ = torch.max(F.softmax(prob_volume_pre, dim=1), dim=1)
            # photometric_confidence = torch.max(prob_volume, dim=1)[0] / torch.sum(prob_volume, dim=1)
        else:
            raise NotImplementedError("Don't support {}!".format(self.mode))

        return {"depth": depth, "photometric_confidence": photometric_confidence, "prob_volume": prob_volume,
                "depth_values": depth_values, "interval": interval}

class CostAgg(nn.Module):
    def __init__(self, mode="variance", in_channels=None):
        super(CostAgg, self).__init__()
        self.mode = mode
        assert mode in ("variance", "adaptive"), "Don't support {}!".format(mode)
        if self.mode == "adaptive":
            self.weight_net = nn.ModuleList([AggWeightNetVolume(in_channels[i]) for i in range(len(in_channels))])

    def forward(self, features, proj_matrices, depth_values, stage_idx):
        """
        :param stage_idx: stage
        :param features: [ref_fea, src_fea1, src_fea2, ...], fea shape: (b, c, h, w)
        :param proj_matrices: (b, nview, ...) [ref_proj, src_proj1, src_proj2, ...]
        :param depth_values: (b, ndepth, h, w)
        :return: matching cost volume (b, c, ndepth, h, w)
        """
        ref_feature, src_features = features[0], features[1:]
        proj_matrices = torch.unbind(proj_matrices, 1)  # to list
        ref_proj, src_projs = proj_matrices[0], proj_matrices[1:]

        num_views = len(features)
        num_depth = depth_values.shape[1]

        ref_volume = ref_feature.unsqueeze(2).repeat(1, 1, num_depth, 1, 1)
        if self.mode == "variance":
            volume_sum = ref_volume
            volume_sq_sum = ref_volume ** 2
            del ref_volume
        elif self.mode == "adaptive":
            volume_adapt = None

        for src_fea, src_proj in zip(src_features, src_projs):
            # warpped features
            src_proj_new = src_proj[:, 0].clone()
            src_proj_new[:, :3, :4] = torch.matmul(src_proj[:, 1, :3, :3], src_proj[:, 0, :3, :4])
            ref_proj_new = ref_proj[:, 0].clone()
            ref_proj_new[:, :3, :4] = torch.matmul(ref_proj[:, 1, :3, :3], ref_proj[:, 0, :3, :4])
            warped_volume = homo_warping(src_fea, src_proj_new, ref_proj_new, depth_values)

            if self.mode == "variance":
                volume_sum = volume_sum + warped_volume
                volume_sq_sum = volume_sq_sum + warped_volume ** 2
            elif self.mode == "adaptive":
                # (b, c, d, h, w)
                warped_volume = (ref_volume - warped_volume).pow_(2)
                weight = self.weight_net[stage_idx](warped_volume)
                if volume_adapt is None:
                    volume_adapt = (weight + 1) * warped_volume
                else:
                    volume_adapt = volume_adapt + (weight + 1) * warped_volume

            del warped_volume

        # aggregate multiple feature volumes by variance
        if self.mode == "variance":
            volume_variance = volume_sq_sum.div_(num_views).sub_(volume_sum.div_(num_views).pow_(2))
            return volume_variance
        elif self.mode == "adaptive":
            return volume_adapt / (num_views - 1)

class MVSNet(nn.Module):
    def __init__(self, ndepths, depth_interval_ratio, cr_base_chs=None, fea_mode="fpn", agg_mode="variance", depth_mode="regression"):
        super(MVSNet, self).__init__()

        if cr_base_chs is None:
            cr_base_chs = [8] * len(ndepths)
        self.ndepths = ndepths
        self.depth_interval_ratio = depth_interval_ratio
        self.fea_mode = fea_mode
        self.cr_base_chs = cr_base_chs
        self.num_stage = len(ndepths)

        print("netphs:", ndepths)
        print("depth_intervals_ratio:", depth_interval_ratio)
        print("cr_base_chs:", cr_base_chs)
        print("fea_mode:", fea_mode)
        print("agg_mode:", agg_mode)
        print("depth_mode:", depth_mode)

        assert len(ndepths) == len(depth_interval_ratio)

        self.feature = FeatureNet(base_channels=8, stride=4, num_stage=self.num_stage, mode=self.fea_mode)
        self.cost_aggregation = CostAgg(agg_mode, self.feature.out_channels)

        self.cost_regularization = nn.ModuleList(
            [CostRegNet(in_channels=self.feature.out_channels[i], base_channels=self.cr_base_chs[i]) for i in range(self.num_stage)])

        self.DepthNet = DepthNet(depth_mode)

    def forward(self, imgs, proj_matrices, depth_values):
        """
        :param is_flip: augment only for 3D-UNet
        :param imgs: (b, nview, c, h, w)
        :param proj_matrices:
        :param depth_values:
        :return:
        """
        depth_interval = (depth_values[0, -1] - depth_values[0, 0]) / depth_values.size(1)

        # step 1. feature extraction
        features = []
        for nview_idx in range(imgs.size(1)):  # imgs shape (B, N, C, H, W)
            img = imgs[:, nview_idx]
            features.append(self.feature(img))

        ori_shape = imgs[:, 0].shape[2:]  # (H, W)

        outputs = {}
        last_depth = None
        for stage_idx in range(self.num_stage):
            # print("*********************stage{}*********************".format(stage_idx + 1))
            # stage feature, proj_mats, scales
            features_stage = [feat["stage{}".format(stage_idx + 1)] for feat in features]
            proj_matrices_stage = proj_matrices["stage{}".format(stage_idx + 1)]
            # stage1: 1/4, stage2: 1/2, stage3: 1
            stage_scale = 2 ** (3 - stage_idx - 1)

            stage_shape = [ori_shape[0] // int(stage_scale), ori_shape[1] // int(stage_scale)]

            if stage_idx == 0:
                last_depth = depth_values
            else:
                last_depth = last_depth.detach()

            # (B, D, H, W)
            depth_range_samples, interval = get_depth_range_samples(last_depth=last_depth,
                                                                    ndepth=self.ndepths[stage_idx],
                                                                    depth_inteval_pixel=self.depth_interval_ratio[
                                                                                stage_idx] * depth_interval,
                                                                    shape=stage_shape  # only for first stage
                                                                    )

            if stage_idx > 0:
                depth_range_samples = F.interpolate(depth_range_samples, stage_shape, mode='bilinear', align_corners=Align_Corners_Range)

            # (b, c, d, h, w)
            cost_volume = self.cost_aggregation(features_stage, proj_matrices_stage, depth_range_samples, stage_idx)
            # cost volume regularization
            # (b, 1, d, h, w)
            cost_reg = self.cost_regularization[stage_idx](cost_volume)

            # depth
            outputs_stage = self.DepthNet(cost_reg, depth_range_samples, num_depth=self.ndepths[stage_idx], interval=interval)

            last_depth = outputs_stage['depth']

            outputs["stage{}".format(stage_idx + 1)] = outputs_stage
            outputs.update(outputs_stage)

        return outputs


# Copyright (c) 2018 Patricio Cubillos and contributors.
# eclipse is open-source software under the MIT license (see LICENSE).

__all__ = ["eclipse", "mandelecl"]

import os
import sys
import numpy as np

libdir = os.path.realpath(os.path.dirname(__file__) + "/lib")
sys.path.append(libdir)
import _eclipse as ecl

class Ecl(object):
  """
  Eclipse-model superclass.
  """
  def __init__(self, time=None, mask=None, params=None):
    self.type = "astro"
    self.npars = len(self.pnames)
    self.params = np.zeros(self.npars)
    if params is not None:
      self.params[:] = params
    self.pmin   = np.tile(-np.inf, self.npars)
    self.pmax   = np.tile( np.inf, self.npars)
    self.pstep  = np.zeros(self.npars)
    if time is not None:
      self.setup(time)

  def __call__(self, params, time=None, mask=None, update=False):
    """
    Call function using self values as defaults.

    Parameters
    ----------
    params: 1D float ndarray
       Model parameters (see class docstring for details).
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    mask: 1D bool ndarray
       Times good-value mask.
    update: Bool
       If True, updated the object's attributes.
    """
    if update:
      self.setup(time, mask, params)

    if time is None:
      time = self.time

    if mask is not None:
      return self.eval(params, time[mask])
    elif self.mask is not None:
      return self.eval(params, time[self.mask])

    return self.eval(params, time)

  def setup(self, time=None, mask=None, params=None, obj=None):
    """
    Set the model's attributes (when not None).

    Parameters
    ----------
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    mask: 1D bool ndarray
       Times good-value mask.
    params: 1D float ndarray
       Model parameters.
    obj: An object
       If not None, extract the time and mask values from the
       object's attributes.
    """
    if obj is not None:
      time = obj.time
      if mask is None:  # Input mask takes priority over obj.mask
        mask = obj.mask
      # FINDME: do I want params?

    # Independent variables:
    if time is not None:
      if mask is None:
        mask = np.ones(len(time), bool)
      self.time = time
      self.mask = mask
    # Model parameters:
    if params is not None:
      self.params[:] = params

class eclipse(Ecl):
  """
  A secondary-eclipse model for the times of the Webb.

  This class implements a Mandel & Agol eclipse model with independent
  ingress and egress depths, and the out-of-eclipse flux as a second-
  degree polynomial.

  This model has nine parameters (self.params, see also self.pnames):
   - midpt: Mid-eclipse epoch.
   - width: Eclipse duration between first and fourth contacts (T14).
   - idepth: Normalized eclipse depth at ingress (T1) relative to a
             stellar flux of 1.0.  That is,
             idepth = (flux(T1) - flux(T2))/flux(T2).
   - edepth: Normalized eclipse depth at egress (T4) relative to a
             stellar flux of 1.0.  That is,
             edepth = (flux(T4) - flux(T3))/flux(T3).
   - ting: Ingress duration (time between first and second contacts, T12).
   - tegr: Egress duration (time between third and fourth contacts, T34).
   - flux: Stellar flux level.  That is, flux during eclipse:
           flux = flux(T2) = flux(T3).
   - slope: Out-of-eclipse linear slope.
   - quad: Out-of-eclipse quadratic term slope.

  The time units are arbitrary, and thus, a user's choice.  Note that
  midpt, width, ting, terg, and time must have consistent units.

  Attributes
  ----------
  name: String
     The model's name.
  type: String
     The type of model.
  pnames: 1D string list
     Names of the model parameters.
  npars: Integer
     The number of model parameters.
  params: 1D float ndarray
     The model parameter values.
  pmin: 1D float ndarray
     Minimum-value boundary of the parameters (for MCMCs).
  pmax: 1D float ndarray
     Maximum-value boundary of the parameters (for MCMCs).
  pstep: 1D float ndarray
     Parameter stepsize (for MCMCs).
  time: 1D float ndarray
     The model's independent variable (timestamps where to evaluate).
  mask: 1D bool ndarray
     Time good-value mask.

  Example
  -------
  >>> import sys
  >>> import numpy as np
  >>> import matplotlib.pyplot as plt

  >>> sys.path.append("../eclipse")
  >>> import eclipse as ecl

  >>> # Create an eclipse model with given orbital-phase timestamps:
  >>> phase = np.linspace(0.35, 0.65, 300)
  >>> model = ecl.eclipse(time=phase)
  >>> # Define eclipse parameters:
  >>> #               [midpt width idepth edepth ting  tegr  flux slope  quad]
  >>> params = np.array([0.5, 0.1, 0.01, 0.008,  0.01, 0.01, 1.0, -0.02, -0.03])
  >>> # One can call the eval function:
  >>> eclipse1 = model.eval(params, phase)
  >>> # Or directly call the object (no need to pass the phase):
  >>> params[8] = 0.0  # Linear-slope model
  >>> eclipse2 = model(params)

  >>> # Show results:
  >>> plt.figure(0)
  >>> plt.clf()
  >>> plt.plot(phase, eclipse1, ".-", color='b')
  >>> plt.plot(phase, eclipse2, ".-", color='orange')
  >>> plt.xlim(0.34, 0.66)
  >>> plt.ylim(0.999, 1.013)
  >>> plt.xlabel("Orbital phase")
  >>> plt.ylabel("Flux")
  """
  def __init__(self, time=None, mask=None, params=None):
    """
    Class constructor.

    Parameters
    ----------
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    mask: 1D bool ndarray
       Times good-value mask.
    params: 1D float ndarray
       Model parameters (see class docstring for details).
    """
    self.name = "eclipse"
    self.pnames = ["midpt", "width", "idepth", "edepth", "ting", "tegr",
                   "flux", "slope", "quad"]
    super(eclipse, self).__init__(time, mask, params)
    # Update pmin:
    self.pmin = np.array([-np.inf, 0, 0, 0, 0, 0, 0, -np.inf, -np.inf])

  def eval(self, params, time):
    """
    Evaluate the eclipse function at the specified times.

    Parameters
    ----------
    params: 1D float ndarray
       Model parameters (see class docstring for details).
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    """
    return ecl.eclipse_quad(params, time)

class mandelecl(Ecl):
  """
  Secondary-eclipse model from Mandel & Agol (2002).

  This model has six parameters (self.params, see also self.pnames):
   - midpt: mid-eclipse epoch.
   - width: Eclipse duration between first and fourth contacts (T14).
   - depth: Normalized eclipse depth relative to a stellar flux of 1.0.
            That is, depth = dflux/flux.
   - ting:  Ingress duration (time between first and second contacts, T12).
   - tegr:  Egress duration (time between third and fourth contacts, T34).
   - flux:  Stellar flux level, i.e., flux during eclipse.

  The time units are arbitrary, and thus, a user's choice.  Note that
  midpt, width, ting, terg, and time must have consistent units.

  Attributes
  ----------
  name: String
     The model's name.
  type: String
     The type of model.
  pnames: 1D string list
     Names of the model parameters.
  npars: Integer
     The number of model parameters.
  params: 1D float ndarray
     The model parameter values.
  pmin: 1D float ndarray
     Minimum-value boundary of the parameters (for MCMCs).
  pmax: 1D float ndarray
     Maximum-value boundary of the parameters (for MCMCs).
  pstep: 1D float ndarray
     Parameter stepsize (for MCMCs).
  time: 1D float ndarray
     The model's independent variable (timestamps where to evaluate).
  mask: 1D bool ndarray
     Time good-value mask.

  Example
  -------
  >>> import sys
  >>> import numpy as np
  >>> import matplotlib.pyplot as plt

  >>> sys.path.append("../eclipse")
  >>> import eclipse as ecl

  >>> # Create a mandelecl model (setting model.time):
  >>> phase = np.linspace(0.35, 0.65, 300)
  >>> model = ecl.mandelecl(phase)
  >>> # Define eclipse parameters:
  >>> #                 midpt width depth ting  tegr  flux
  >>> params = np.array([0.5, 0.1,  0.01, 0.01, 0.01, 1.0])
  >>> # One can call the eval function:
  >>> eclipse1 = model.eval(params, phase)
  >>> # Or directly call the object:
  >>> params[2] = 0.015  # Change eclipse depth
  >>> eclipse2 = model(params, phase)
  >>> # This call can use the default phase, so no need to pass as argument:
  >>> params[5] = 1.005  # Change flux level
  >>> eclipse3 = model(params)

  >>> # Show results:
  >>> plt.figure(0)
  >>> plt.clf()
  >>> plt.plot(phase, eclipse1, ".-", color='b')
  >>> plt.plot(phase, eclipse2, ".-", color='orange')
  >>> plt.plot(phase, eclipse3, ".-", color='limegreen')
  >>> plt.xlim(0.34, 0.66)
  >>> plt.xlabel("Orbital phase")
  >>> plt.ylabel("Flux")
  """
  def __init__(self, time=None, mask=None, params=None):
    """
    Class constructor.

    Parameters
    ----------
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    mask: 1D bool ndarray
       Times good-value mask.
    params: 1D float ndarray
       Model parameters (see class docstring for details).
    """
    self.name = "mandelecl"
    self.pnames = ["midpt",  "width",  "depth",  "ting",  "tegr",  "flux"]
    super(mandelecl, self).__init__(time, mask, params)
    # Update pmin:
    self.pmin   = np.array([-np.inf, 0.0, 0.0, 0.0, 0.0, -np.inf])

  def eval(self, params, time):
    """
    Evaluate the eclipse function at the specified times.

    Parameters
    ----------
    params: 1D float ndarray
       Model parameters (see class docstring for details).
    time: 1D float ndarray
       Phase/times where to evaluate the model.
    """
    return ecl.mandelecl(params, time)


"""
CoordinateMaps map (transform) an image from an input space to an output space.

A CoordinateMap object contains all the details about an input
CoordinateSystem, an output CoordinateSystem and the mappings between
them.  The *mapping* transforms an image from the input coordinate
system to the output coordinate system.  And the *inverse_mapping*
performs the opposite transformation.  The *inverse_mapping* can be
specified explicity when creating the CoordinateMap or implicitly in the
case of an Affine CoordinateMap.

"""

"""
Matthew, Cindee thoughts

Should we change the order of input args to CoordinateMap to:

CoordinateMap(input_coords, output_coords, mapping, inverse_mapping=None)
or keep as
CoordinateMap(mapping, input_coords, output_coords, inverse_mapping=None)

CoordinateMap.ndim should be renamed to CoordinateMap.ndims, because it returns more than one value.

Affine should be renamed to AffineMap, or AffineCoordMap, or something

We need to think about whether coordinate values should be N by 3
(where 3 is the number of coordinates in the coordinate system), or 3
by N.

3 by N makes more sense when we think of applying an affine matrix to the points.

We might think, that if we have a matrix ``arr`` with points in, then
``arr[0]`` should be the first point [x,y,z], rather then the first coordinate
value of all the points - N values of [x]

Consider renaming reorder_input to self.input_reordered() or
something.  reorder_output similarly.

That's as far as we got.

"""

import warnings

import numpy as np

import nipy.core.transforms.affines as affines
from nipy.core.reference.coordinate_system import(CoordinateSystem,
                                                          safe_dtype)
from nipy.core.reference.coordinate_system import product as coordsys_product

__docformat__ = 'restructuredtext'

class CoordinateMap(object):
    """A set of input and output CoordinateSystems and a mapping between them.

    For example, the mapping may represent the mapping of an image
    from voxel space (the input coordinates) to real space (the output
    coordinates).  The mapping may be an affine or non-affine
    transformation.

    Attributes
    ----------
    input_coords : :class:`CoordinateSystem`
        The input coordinate system.
    output_coords : :class:`CoordinateSystem`
        The output coordinate system.
    mapping : callable
        A callable that maps the input_coords to the output_coords.
    inverse_mapping : None or callable
        A callable that maps the output_coords to the input_coords.
        Not all mappings have an inverse, in which case
        inverse_mapping is None.

    Examples
    --------
    >>> input_coords = CoordinateSystem('ijk', 'voxels')
    >>> output_coords = CoordinateSystem('xyz', 'world')
    >>> mni_orig = np.array([-90.0, -126.0, -72.0])
    >>> mapping = lambda x: x + mni_orig
    >>> inv_mapping = lambda x: x - mni_orig
    >>> cm = CoordinateMap(mapping, input_coords, output_coords, inv_mapping)

    Map the first 3 voxel coordinates, along the x-axis, to mni space:

    >>> x = np.array([[0,0,0], [1,0,0], [2,0,0]])
    >>> cm.mapping(x)
    array([[ -90., -126.,  -72.],
           [ -89., -126.,  -72.],
           [ -88., -126.,  -72.]])

    """
    def __init__(self, mapping,
                 input_coords,
                 output_coords,
                 inverse_mapping=None):
        """Create a CoordinateMap given the input/output coords and mappings.

        Parameters
        ----------
        mapping : callable
           The mapping between input and output coordinates
        input_coords : :class:`CoordinateSystem`
           The input coordinate system
        output_coords : :class:`CoordinateSystem`
           The output coordinate system
        inverse_mapping : None or callable, optional
           The optional inverse of mapping, with the intention being
           ``x = inverse_mapping(mapping(x))``.  If the mapping is
           affine and invertible, then this is true for all x.  The
           default is None

        Returns
        -------
        coordmap : CoordinateMap
        """
        # These attrs define the structure of the coordmap.
        self._mapping = mapping
        self._input_coords = input_coords
        self._output_coords = output_coords
        self._inverse_mapping = inverse_mapping

        if not callable(mapping):
            raise ValueError('The mapping must be callable.')
        if inverse_mapping is not None:
            if not callable(inverse_mapping):
                raise ValueError('The inverse_mapping must be callable.')
        self._checkmapping()

    @property
    def input_coords(self):
        'input coordinate system'
        return self._input_coords

    @property
    def output_coords(self):
        'output coordinate system'
        return self._output_coords

    @property
    def mapping(self):
        'The mapping from input_coords to output_coords.'
        return self._mapping

    @property
    def inverse_mapping(self):
        'The mapping from output_coords to input_coords'
        return self._inverse_mapping

    @property
    def inverse(self):
        """
        Return a new CoordinateMap with the mappings reversed
        """
        if self._inverse_mapping is None:
            return None
        return CoordinateMap(self._inverse_mapping,
                             self._output_coords,
                             self._input_coords,
                             inverse_mapping=self._mapping)

    @property
    def ndim(self):
        'Number of dimensions of input and output coordinates.'
        return (self._input_coords.ndim, self._output_coords.ndim)

    def _checkmapping(self):
        """Verify that the input and output dimensions of self.mapping work.

        We do this by passing something that should work, through __call__
        """
        inp = np.zeros((10, self.ndim[0]),
                       dtype=self._input_coords.coord_dtype)
        out = self(inp)

    def __call__(self, x):
        """Return mapping evaluated at x

        Check input and output of mapping for compatiblity with input
        and output coordinate systems respectively.

        Parameters
        ----------
        x : array-like
           Values in input coordinate system space that will be mapped
           to the output coordinate system space, using
           ``self.mapping``

        Returns
        -------
        y : array
           Values in output coordinate system space

        Examples
        --------
        >>> input_cs = CoordinateSystem('ijk')
        >>> output_cs = CoordinateSystem('xyz')
        >>> mapping = lambda x:x+1
        >>> inverse = lambda x:x-1
        >>> cm = CoordinateMap(mapping, input_cs, output_cs, inverse)
        >>> cm([2,3,4])
        array([[3, 4, 5]])
        >>> cmi = cm.inverse
        >>> cmi([2,6,12])
        array([[ 1,  5, 11]])

        """

        in_vals = self._input_coords._checked_values(x)
        out_vals = self._mapping(in_vals)
        return self._output_coords._checked_values(out_vals)

    def copy(self):
        """Create a copy of the coordmap.

        Returns
        -------
        coordmap : CoordinateMap

        """

        return CoordinateMap(self._mapping,
                             self._input_coords,
                             self._output_coords,
                             inverse_mapping=self._inverse_mapping)

class Affine(CoordinateMap):
    """
    A class representing an affine transformation from an input
    coordinate system to an output coordinate system.

    This class has an affine property, which is a matrix representing
    the affine transformation in homogeneous coordinates.  This matrix
    is used to perform mappings, rather than having an explicit
    mapping function.

    >>> inp_cs = CoordinateSystem('ijk')
    >>> out_cs = CoordinateSystem('xyz')
    >>> cm = Affine(np.diag([1, 2, 3, 1]), inp_cs, out_cs)
    >>> cm.affine
    array([[ 1.,  0.,  0.,  0.],
           [ 0.,  2.,  0.,  0.],
           [ 0.,  0.,  3.,  0.],
           [ 0.,  0.,  0.,  1.]])
    >>> cm([1,1,1])
    array([[ 1.,  2.,  3.]])
    >>> icm = cm.inverse
    >>> icm([1,2,3])
    array([[ 1.,  1.,  1.]])
    """

    def __init__(self, affine, input_coords, output_coords):
        """
        Return an CoordinateMap specified by an affine transformation
        in homogeneous coordinates.

        Parameters
        ----------
        affine : array-like
           affine homogenous coordinate matrix
        input_coords : :class:`CoordinateSystem`
           input coordinates
        output_coords : :class:`CoordinateSystem`
           output coordinates

        Notes
        -----
        The dtype of the resulting matrix is determined by finding a
        safe typecast for the input_coords, output_coords and affine.
        """
        affine = np.asarray(affine)
        dtype = safe_dtype(affine.dtype,
                           input_coords.coord_dtype,
                           output_coords.coord_dtype)
        inaxes = input_coords.coord_names
        outaxes = output_coords.coord_names
        self._input_coords = CoordinateSystem(inaxes,
                                              input_coords.name,
                                              dtype)
        self._output_coords = CoordinateSystem(outaxes,
                                               output_coords.name,
                                               dtype)
        affine = np.asarray(affine, dtype=dtype)
        if affine.shape != (self.ndim[1]+1, self.ndim[0]+1):
            raise ValueError('coordinate lengths do not match '
                             'affine matrix shape')
        self._affine = affine
        A, b = affines.to_matrix_vector(affine)
        def _mapping(x):
            value = np.dot(x, A.T)
            value += b
            return value
        self._mapping = _mapping

    @property
    def affine(self):
        """The affine transform matrix of the Affine CoordinateMap."""
        return self._affine

    @property
    def inverse_mapping(self):
        """The inverse affine mapping from the Affine CoordinateMap."""
        inverse = self.inverse
        if inverse is None:
            raise ValueError('There is no inverse for this affine')
        return inverse.mapping

    @property
    def inverse(self):
        """
        Return the inverse coordinate map.
        """
        try:
            return Affine(np.linalg.inv(self.affine),
                          self.output_coords,
                          self.input_coords)
        except np.linalg.linalg.LinAlgError:
            pass

    @staticmethod
    def from_params(innames, outnames, params):
        """
        Create an `Affine` instance from sequences of innames and outnames.

        Parameters
        ----------
        innames : ``tuple`` of ``string``
           The names of the axes of the input coordinate systems
        outnames : ``tuple`` of ``string``
           The names of the axes of the output coordinate systems
        params : `Affine`, `ndarray` or `(ndarray, ndarray)`
           An affine mapping between the input and output coordinate
           systems.  This can be represented either by a single
           ndarray (which is interpreted as the representation of the
           mapping in homogeneous coordinates) or an (A,b) tuple.

        Returns
        -------
        aff : `Affine` object instance

        Notes
        -----
        :Precondition: ``len(shape) == len(names)``

        :Raises ValueError: ``if len(shape) != len(names)``
        """
        if type(params) == type(()):
            A, b = params
            params = affines.from_matrix_vector(A, b)

        ndim = (len(innames) + 1, len(outnames) + 1)
        if params.shape != ndim[::-1]:
            raise ValueError('shape and number of axis names do not agree')
        dtype = params.dtype

        input_coords = CoordinateSystem(innames, "input")
        output_coords = CoordinateSystem(outnames, 'output')
        return Affine(params, input_coords, output_coords)

    @staticmethod
    def from_start_step(innames, outnames, start, step):
        """
        Create an `Affine` instance from sequences of names, start
        and step.

        Parameters
        ----------
        innames : ``tuple`` of ``string``
            The names of the axes of the input coordinate systems
        outnames : ``tuple`` of ``string``
            The names of the axes of the output coordinate systems
        start : ``tuple`` of ``float``
            Start vector used in constructing affine transformation
        step : ``tuple`` of ``float``
            Step vector used in constructing affine transformation

        Returns
        -------
        cm : `CoordinateMap`

        Examples
        --------
        >>> cm = Affine.from_start_step('ijk', 'xyz', [1, 2, 3], [4, 5, 6])
        >>> cm.affine
        array([[ 4.,  0.,  0.,  1.],
               [ 0.,  5.,  0.,  2.],
               [ 0.,  0.,  6.,  3.],
               [ 0.,  0.,  0.,  1.]])

        Notes
        -----
        ``len(names) == len(start) == len(step)``

        """
        ndim = len(innames)
        if len(outnames) != ndim:
            raise ValueError('len(innames) != len(outnames)')
        return Affine.from_params(innames,
                                  outnames,
                                  (np.diag(step), start))

    @staticmethod
    def identity(names):
        """
        Return an identity coordmap of the given shape.

        Parameters
        ----------
        names : ``tuple`` of ``string``
           Names of Axes in output CoordinateSystem

        Returns
        -------
        cm : `CoordinateMap`
           ``CoordinateMap`` with `CoordinateSystem` input and an
           identity transform, with identical input and output coords.

        Examples
        --------
        >>> cm = Affine.identity('ijk')
        >>> cm.affine
        array([[ 1.,  0.,  0.,  0.],
               [ 0.,  1.,  0.,  0.],
               [ 0.,  0.,  1.,  0.],
               [ 0.,  0.,  0.,  1.]])
        >>> print cm.input_coords
        name: 'input', coord_names: ('i', 'j', 'k'), coord_dtype: float64
        >>> print cm.output_coords
        name: 'output', coord_names: ('i', 'j', 'k'), coord_dtype: float64
        """
        return Affine.from_start_step(names, names, [0]*len(names),
                                      [1]*len(names))

    def copy(self):
        """
        Create a copy of the coordmap.

        Returns
        -------
        cm : `CoordinateMap`

        Examples
        --------
        >>> cm = Affine(np.eye(4), CoordinateSystem('ijk'), CoordinateSystem('xyz'))
        >>> cm_copy = cm.copy()
        >>> cm is cm_copy
        False

        Note that the matrix (affine) is not a pointer to the
        same data, it's a full independent copy

        >>> cm.affine[0,0] = 2.0
        >>> cm_copy.affine[0,0]
        1.0
        """
        return Affine(self._affine.copy(), self._input_coords,
                      self._output_coords)

def reorder_input(coordmap, order=None):
    """
    Create a new coordmap with reversed input_coords.
    Default behaviour is to reverse the order of the input_coords.
    If the coordmap has a shape, the resulting one will as well.

    Inputs:
    -------
    order: sequence
         Order to use, defaults to reverse. The elements
         can be integers, strings or 2-tuples of strings.
         If they are strings, they should be in coordmap.input_coords.coord_names.

    Returns:
    --------

    newcoordmap: `CoordinateMap`
         A new CoordinateMap with reversed input_coords.

    >>> input_cs = CoordinateSystem('ijk')
    >>> output_cs = CoordinateSystem('xyz')
    >>> cm = Affine(np.identity(4), input_cs, output_cs)
    >>> print reorder_input(cm, 'ikj').input_coords
    name: '-reordered', coord_names: ('i', 'k', 'j'), coord_dtype: float64
    """
    ndim = coordmap.ndim[0]
    if order is None:
        order = range(ndim)[::-1]
    elif type(order[0]) == type(''):
        order = [coordmap.input_coords.index(s) for s in order]

    newaxes = [coordmap.input_coords.coord_names[i] for i in order]
    newincoords = CoordinateSystem(newaxes,
                                   coordmap.input_coords.name + '-reordered',
                                   coord_dtype=coordmap.input_coords.coord_dtype)
    perm = np.zeros((ndim+1,)*2)
    perm[-1,-1] = 1.

    for i, j in enumerate(order):
        perm[j,i] = 1.

    perm = perm.astype(coordmap.input_coords.coord_dtype)
    A = Affine(perm, newincoords, coordmap.input_coords)
    return compose(coordmap, A)

def reorder_output(coordmap, order=None):
    """
    Create a new coordmap with reversed output_coords.
    Default behaviour is to reverse the order of the input_coords.

    Inputs:
    -------

    order: sequence
         Order to use, defaults to reverse. The elements
         can be integers, strings or 2-tuples of strings.
         If they are strings, they should be in coordmap.output_coords.coord_names.

    Returns:
    --------

    newcoordmap: `CoordinateMap`
         A new CoordinateMap with reversed output_coords.

    >>> input_cs = CoordinateSystem('ijk')
    >>> output_cs = CoordinateSystem('xyz')
    >>> cm = Affine(np.identity(4), input_cs, output_cs)
    >>> print reorder_output(cm, 'xzy').output_coords
    name: '-reordered', coord_names: ('x', 'z', 'y'), coord_dtype: float64
    >>> print reorder_output(cm, [0,2,1]).output_coords.coord_names
    ('x', 'z', 'y')

    >>> newcm = reorder_output(cm, 'yzx')
    >>> newcm.output_coords.coord_names
    ('y', 'z', 'x')

    """

    ndim = coordmap.ndim[1]
    if order is None:
        order = range(ndim)[::-1]
    elif type(order[0]) == type(''):
        order = [coordmap.output_coords.index(s) for s in order]

    newaxes = [coordmap.output_coords.coord_names[i] for i in order]
    newoutcoords = CoordinateSystem(newaxes, coordmap.output_coords.name + '-reordered', coordmap.output_coords.coord_dtype)

    perm = np.zeros((ndim+1,)*2)
    perm[-1,-1] = 1.

    for i, j in enumerate(order):
        perm[j,i] = 1.

    perm = perm.astype(coordmap.output_coords.coord_dtype)
    A = Affine(perm, coordmap.output_coords, newoutcoords)
    return compose(A, coordmap)

def product(*cmaps):
    """
    Return the "topological" product of two or more CoordinateMaps.

    Inputs:
    -------
    cmaps : sequence of CoordinateMaps

    Returns:
    --------
    cmap : ``CoordinateMap``

    >>> inc1 = Affine.from_params('i', 'x', np.diag([2,1]))
    >>> inc2 = Affine.from_params('j', 'y', np.diag([3,1]))
    >>> inc3 = Affine.from_params('k', 'z', np.diag([4,1]))

    >>> cmap = product(inc1, inc3, inc2)
    >>> cmap.input_coords.coord_names
    ('i', 'k', 'j')
    >>> cmap.output_coords.coord_names
    ('x', 'z', 'y')
    >>> cmap.affine
    array([[ 2.,  0.,  0.,  0.],
           [ 0.,  4.,  0.,  0.],
           [ 0.,  0.,  3.,  0.],
           [ 0.,  0.,  0.,  1.]])

    """
    ndimin = [cmap.ndim[0] for cmap in cmaps]
    ndimin.insert(0,0)
    ndimin = tuple(np.cumsum(ndimin))

    def mapping(x):
        x = np.asarray(x)
        y = []
        for i in range(len(ndimin)-1):
            cmap = cmaps[i]
            if x.ndim == 2:
                yy = cmaps[i](x[:,ndimin[i]:ndimin[i+1]])
            else:
                yy = cmaps[i](x[ndimin[i]:ndimin[i+1]])
            y.append(yy)
        yy = np.hstack(y)
        return yy

    notaffine = filter(lambda x: not isinstance(x, Affine), cmaps)

    incoords = coordsys_product(*[cmap.input_coords for cmap in cmaps])
    outcoords = coordsys_product(*[cmap.output_coords for cmap in cmaps])

    if not notaffine:

        affine = linearize(mapping, ndimin[-1], dtype=incoords.coord_dtype)
        return Affine(affine, incoords, outcoords)
    return CoordinateMap(mapping, incoords, outcoords)

def compose(*cmaps):
    """
    Return the composition of two or more CoordinateMaps.

    Inputs:
    -------
    cmaps : sequence of CoordinateMaps

    Returns:
    --------
    cmap : ``CoordinateMap``
         The resulting CoordinateMap has input_coords == cmaps[-1].input_coords
         and output_coords == cmaps[0].output_coords

    >>> cmap = Affine.from_params('i', 'x', np.diag([2.,1.]))
    >>> cmapi = cmap.inverse
    >>> id1 = compose(cmap,cmapi)
    >>> print id1.affine
    [[ 1.  0.]
     [ 0.  1.]]

    >>> id2 = compose(cmapi,cmap)
    >>> id1.input_coords.coord_names
    ('x',)
    >>> id2.input_coords.coord_names
    ('i',)
    >>>

    """

    def _compose2(cmap1, cmap2):
        forward = lambda input: cmap1.mapping(cmap2.mapping(input))
        if cmap1.inverse is not None and cmap2.inverse is not None:
            backward = lambda output: cmap2.inverse.mapping(cmap1.inverse.mapping(output))
        else:
            backward = None
        return forward, backward

    cmap = cmaps[-1]
    for i in range(len(cmaps)-2,-1,-1):
        m = cmaps[i]
        if m.input_coords == cmap.output_coords:
            forward, backward = _compose2(m, cmap)
            cmap = CoordinateMap(forward,
                                 cmap.input_coords,
                                 m.output_coords,
                                 inverse_mapping=backward)
        else:
            raise ValueError(
                'input and output coordinates do not match: '
                'input=%s, output=%s' %
                (`m.input_coords.dtype`, `cmap.output_coords.dtype`))

    notaffine = filter(lambda cmap: not isinstance(cmap, Affine), cmaps)
    if not notaffine:
        affine = linearize(cmap,
                           cmap.ndim[0],
                           dtype=cmap.output_coords.coord_dtype)
        return Affine(affine, cmap.input_coords,
                      cmap.output_coords)
    return cmap

def replicate(coordmap, n, concataxis='concat'):
    """
    Create a CoordinateMap by taking the product
    of coordmap with a 1-dimensional 'concat' CoordinateSystem

    :Parameters:
         coordmap : `CoordinateMap`
                The coordmap to be used
         n : ``int``
                The number of tiems to concatenate the coordmap
         concataxis : ``string``
                The name of the new dimension formed by concatenation
    """

    raise NotImplementedError('The method this function depends on'
                              'no longer exists.')
    """
    concat = CoordinateMap.from_affine([concataxis], [concataxis],
                                       Affine(np.identity(2)), (n,))
    return product(concat, coordmap)
    """

def linearize(mapping, ndimin, step=1, origin=None, dtype=None):
    """
    Given a Mapping of ndimin variables, return the linearization of
    mapping at origin based on a given step size in each coordinate
    axis.

    If not specified, origin defaults to np.zeros(ndimin, dtype=dtype).

    Parameters
    ----------
    mapping : callable
       A function to linearize
    ndimin : int
       Number of input dimensions to mapping
    step : scalar, optional
       step size over which to calculate linear components.  Default 1
    origin : None or array, optional
       Origin at which to linearize mapping.  If None, origin is
       ``np.zeros(ndimin)``
    dtype : None or np.dtype, optional
       dtype for return.  Default is None.  If ``dtype`` is None, and
       ``step`` is an ndarray, use ``step.dtype``.  Otherwise use
       np.float.

    Returns
    -------
    C : array
       Linearization of mapping in homogeneous coordinates, i.e.  an
       array of size (ndimout+1, ndimin+1) where ndimout =
       mapping(origin).shape[0].
    """
    if dtype is None:
        try:
            dtype = step.dtype
        except AttributeError:
            dtype = np.float
    step = np.array(step, dtype=dtype)
    if origin is None:
        origin = np.zeros(ndimin, dtype)
    else:
        if origin.dtype != dtype:
            warnings.warn('origin.dtype != dtype in function linearize, using input dtype')
        origin = np.asarray(origin, dtype=dtype)
        if origin.shape != (ndimin,):
            raise ValueError('origin.shape != (%d,)' % ndimin)
    b = mapping(origin)

    origin = np.multiply.outer(np.ones(ndimin, dtype), origin)
    y1 = mapping(step*np.eye(ndimin, dtype=dtype) + origin)
    y0 = mapping(origin)

    ndimout = y1.shape[1]
    C = np.zeros((ndimout+1, ndimin+1), (y0/step).dtype)
    C[-1,-1] = 1
    C[:ndimout,-1] = b
    C[:ndimout,:ndimin] = (y1 - y0).T / step
    return C

def drop_io_dim(cm, name):
    ''' Drop dimension from coordinate map, if orthogonal to others

    Drops both input and corresponding output dimension.  Thus there
    should be a corresponding input dimension for a selected output
    dimension, or a corresponding output dimension for an input
    dimension.

    Parameters
    ----------
    cm : Affine
       Affine coordinate map instance
    name : str
       Name of input or output dimension to drop.  If this is an input
       dimension, there must be a corresponding output dimension with
       the same index, and vica versa.  The check for orthogonality
       ensures that the input and output dimensions are only related to
       each other, and not to the other dimensions.

    Returns
    -------
    cm_redux : Affine
       Affine coordinate map with orthogonal input + output dimension
       dropped

    Examples
    --------
    Typical use is in getting a 3D coordinate map from 4D

    >>> cm4d = Affine.from_params('ijkl', 'xyzt', np.diag([1,2,3,4,1]))
    >>> cm3d = drop_io_dim(cm4d, 't')
    >>> cm3d.affine
    array([[ 1.,  0.,  0.,  0.],
           [ 0.,  2.,  0.,  0.],
           [ 0.,  0.,  3.,  0.],
           [ 0.,  0.,  0.,  1.]])
    '''
    aff = cm.affine
    in_dims = list(cm.input_coords.coord_names)
    nin = len(in_dims)
    out_dims = list(cm.output_coords.coord_names)
    nout = len(out_dims)
    try:
        i = in_dims.index(name)
    except ValueError:
        try:
            i = out_dims.index(name)
        except ValueError:
            raise ValueError('No input or output dimension '
                             'with name (%s)' % name)
    col_inds = range(nin)
    row_inds = range(nout)
    try:
        col_inds.remove(i)
    except IndexError:
        raise ValueError('Should be corresponding input and '
                         'output dims')
    try:
        row_inds.remove(i)
    except IndexError:
        raise ValueError('Should be corresponding input and '
                         'output dims')
    removed_col = aff[row_inds, i]
    if np.any(removed_col):
        raise ValueError('Elements in dimension %d to remove (%s) '
                         'appear not to be orthogonal '
                         'to remaining dimensions' % (i, removed_col))
    aff = aff[:, col_inds + [nin]]
    aff = aff[row_inds + [nout],:]
    in_dims = [n for i, n in enumerate(in_dims) if i in col_inds]
    out_dims = [n for i, n in enumerate(out_dims) if i in row_inds]
    return Affine.from_params(in_dims, out_dims, aff)

def append_io_dim(cm, in_name, out_name, start=0, step=1):
    ''' Append input and output dimension to coordmap

    Parameters
    ----------
    cm : Affine
       Affine coordinate map instance to which to append dimension
    in_name : str
       Name for new input dimension
    out_name : str
       Name for new output dimension
    start : float, optional
       Offset for transformed values in new dimension
    step : float, optional
       Step, or scale factor for transformed values in new dimension

    Returns
    -------
    cm_plus : Affine
       New coordinate map with appended dimension

    Examples
    --------
    Typical use is creating a 4D coordinate map from a 3D

    >>> cm3d = Affine.from_params('ijk', 'xyz', np.diag([1,2,3,1]))
    >>> cm4d = append_io_dim(cm3d, 'l', 't', 9, 5)
    >>> cm4d.affine
    array([[ 1.,  0.,  0.,  0.,  0.],
           [ 0.,  2.,  0.,  0.,  0.],
           [ 0.,  0.,  3.,  0.,  0.],
           [ 0.,  0.,  0.,  5.,  9.],
           [ 0.,  0.,  0.,  0.,  1.]])
    '''
    aff = cm.affine
    in_dims = list(cm.input_coords.coord_names)
    nin = len(in_dims)
    out_dims = list(cm.output_coords.coord_names)
    nout = len(out_dims)
    in_dims.append(in_name)
    out_dims.append(out_name)
    aff_plus = np.zeros((nout+2, nin+2))
    aff_plus[:nout,:nin] = aff[:nout, :nin]
    aff_plus[:nout,-1] = aff[:nout,-1]
    aff_plus[nout,nin] = step
    aff_plus[-1,-1] = 1
    aff_plus[nout,-1] = start
    return Affine.from_params(in_dims, out_dims, aff_plus)