# -*- coding: utf-8 -*-
"""
Photosynthesis and Stomatal Conductance Model
Created 9/27/2016
Katherine Wentz

This is a program that runs photosynthesis and
stomatal conductance models given changes in leaf-
level traits.

The end product is graphs of NUE vs. WUE.

Update: I am going to run the model for plants with
traits that are distinctive of the meadow moisture
gradient in the alpine tundra.

Fix: correct for atmospheric pressure differences in co2, o2, and vapor pressure

Fix: vcmax temp dependence (pg 63 in plant physiological ecology book)

Fix: NEW VARIBALE TRAIT-->make the fraction of leaf N in rubisco go down with increasing SLA,
chlorophyll content, and decreasing light (wet meadow)--more N is allocated
to thylakoids. The only way for chl/m2 to increase even when g N/m2 goes down
or is constant is for the leaf to allocate more of leaf N to chl...also, note
that there is more organic N designated to photo in leaf when SLA goes up
because less N is used in structure. see "Photosynthesis or persistence: N allocation
in leaves of evergreen and deciduous... by Takashima et al. 2004. Also see Photosynthetic
nitrogen-use efficiency of species...by Poorter and Evans 1998

Note to self: NUE and WUE relationship flipflops with change in air temperature;
NUE makes sense because C:N decreases from dry to wet meadows; WUE increasing
in snowbed does not necessarilly make sense--look in the literature for this

herbs have a higher NUE

"""

#---------------Import Modules---------------#

import itertools as it
import numpy as np
import operator
from matplotlib import pyplot as plt
from matplotlib import rcParams
from scipy.optimize import curve_fit

#Import combinations of variable parameters
from uncertain_params import monte_carlo_all

#Import photosynthesis model
from Photosynthesis_Model import photo_bound_meso_eqstom as photo

#Import functions to switch between Pa and umol/mol at sea level
from photo_functions import pa_con_atmfrac

#for doing letters on graphs for multiple plots in same figure
def get_axis_limits(ax, scale1=.95,scale2=0.9):
    return ax.get_xlim()[1]*scale1, ax.get_ylim()[1]*scale2

#---------------Determine if I Want to Keep Any of the Variable Parameters Constant---------------#

const_params=[]
for xx in it.combinations(['ht','t'],0): #keep ht and t constant for constant vpd
    const_params+=[xx]

#do this when I do not put any of the variable parameters as constant. instead I
#vary each parameter one at a time while keeping the other parameters constant.
if const_params==[()]:
    const_params=[[-999999]]

#---------------Begin Looping Through Photosynthesis Model---------------#

#each loop is for a constant value, or combinatin of constant values, of variable parameter as determined above
for ii in range(len(const_params)):

    #---------------Initialize Plots---------------#

    ##---Figure With Subplots Blueprint---##

    #fb1=plt.figure(1,figsize=(12,2))
    #axA = fb1.add_subplot(121)
    #axB = fb1.add_subplot(122)

    ##---Figures Without Subplots Blueprint---##

    #--figure 1--#

    #simulated and empirical NUE in plant communities
    fig1, (ax1A,ax1B) = plt.subplots(2,figsize=(9,20),sharex=True)
    ax1A.set_ylabel('NUE ($\mu$mol CO$_2$/g N s)',fontsize=20, fontname='Times New Roman')
    ax1A.set_ylim([0,5])
#    ax1A.set_title('NUE in Plant Communities', fontname='Times New Roman',fontsize=30)

    ax1B.set_ylabel('NUE (g biomass/g N)',fontsize=20, fontname='Times New Roman')
    ax1B.set_ylim([60,100])

     #-----figure 2----#

    #simulated and empirical WUE in plant communities
    fig2,(ax2A,ax2B,ax2C) = plt.subplots(3,figsize=(9,20),sharex=True,sharey=True)
    ax2B.set_ylabel('WUE ($\mu$mol CO$_2$/mmol H$_2$O)',fontsize=20, fontname='Times New Roman')
    ax2A.set_ylim([0,5])
#    ax2A.set_title('WUE in Plant Communities', fontname='Times New Roman',fontsize=30)

    #--figure 3--#

    #simulated and empirical Assimilation in plant communities
    fig3, (ax3A,ax3B) = plt.subplots(2,figsize=(9,20),sharex=True)
    ax3A.set_ylabel('Assimilation ($\mu$mol CO$_2$/m$^2$s)',fontsize=20, fontname='Times New Roman')
    ax3A.set_ylim([0,30])
#    ax3A.set_title('Assimilation in Plant Communities', fontname='Times New Roman',fontsize=30)

    ax3B.set_ylabel('Growth Rate (g C/m$^2$ day)',fontsize=20, fontname='Times New Roman')
    ax3B.set_ylim([0,5])

    #--figure 4--#

    #leaf height vs. temperature
    fig4,ax4 = plt.subplots(figsize=(11,11))
    ax4.set_xlabel('Leaf Height (cm)',fontsize=25, fontname='Times New Roman')
    ax4.set_ylabel('Difference Between Leaf and Air Temperature ($^\circ$C)',fontsize=25, fontname='Times New Roman')
#    ax4.set_title('Leaf Height vs. Leaf & Air Temperature Difference', fontname='Times New Roman',fontsize=30)

    #-----figure 5----#

    #assimilation vs. stomatal conductance
    fig5,ax5 = plt.subplots(figsize=(11,11))

    ax5.set_xlabel('Assimilation ($\mu$mol CO$_2$/m$^2$s)',fontsize=25, fontname='Times New Roman')
    ax5.set_ylabel('Stomatal Conductance (mol CO$_2$/m$^2$s)',fontsize=25, fontname='Times New Roman')
#    ax5.set_title('Simulated Assimilation vs. Stomatal Conductance', fontname='Times New Roman',fontsize=30)
    ax5.set_ylim([0,0.55])
    ax5.set_xlim([0,25])

    #--figure 6--#

    #vpd vs. wue
    fig6, (ax6A,ax6B) = plt.subplots(2,figsize=(9,20),sharex=False)
 #   ax6A.set_xlabel('VPD (cmol H$_2$O/mol air)',fontsize=25, fontname='Times New Roman')
    ax6A.set_ylabel('WUE ($\mu$mol CO$_2$/mmol H$_2$O)',fontsize=20, fontname='Times New Roman')
#    ax6A.set_title('Vapor Pressure Deficit vs. WUE', fontname='Times New Roman',fontsize=30)
    ax6B.set_xlabel('VPD (cmol H$_2$O/mol air)',fontsize=20, fontname='Times New Roman')
    ax6B.set_ylabel('WUE ($\mu$mol CO$_2$/mmol H$_2$O)',fontsize=20, fontname='Times New Roman')

    #---------------Initialize Arrays for Each Meadow---------------#

    #total nue and wue
    nue_tot=[]
    wue_tot=[]

    #wue and nue arrays
    wue_d=[]
    nue_d=[]
    wue_d_const=[]

    wue_m_const=[]

    wue_w=[]
    wue_w_const=[]
    nue_w=[]

    #gsw arrays

    gsw_d=[]
    gsw_d_tms=[]
    gsw_m=[]
    gsw_m_tms=[]
    gsw_w=[]
    gsw_w_tms=[]

    gs_d=[]
    gs_m=[]
    gs_w=[]
    #assimilation arrays

    A_d=[]
    A_d_tms=[]
    A_m=[]
    A_m_tms=[]
    A_w=[]
    A_w_tms=[]

    #evapo arrays

    E_d=[]
    E_d_tms=[]
    E_m=[]
    E_m_tms=[]
    E_w=[]
    E_w_tms=[]

    #vapor pressure deficit arrays
    vpd_d=[]
    vpd_d_tms=[]
    vpd_m=[]
    vpd_m_tms=[]
    vpd_w=[]
    vpd_w_tms=[]

    #leaf temp
    tl_d=[]
    tl_m=[]
    tl_w=[]

    #---------------Photosynthesis + Stomatal Conductance Model---------------#

    ##---Constant Parameter Arrays for Model---##

    #----Params Used in Model Currently----#

    tk_25=298.16; #absolute temperature at 25 C
    ekc=80500.0 #Activation energy for K of CO2 (J mol-1)
    eko=14500.0 #Activation energy for K of O2 (J mol-1)
    etau=-29000.0  #Activation energy for tau (???) (J mol-1)
    ev=55000.0 #Activation energy for carboxylation (J mol-1)
    ej=55000.0 #Activation energy for electron transport (J mol-1)
    toptv=303.0 #Optimum temperature for maximum carboxylation (K)
    toptj=303.0 #Optimum temperature for maximum electron transport (K)
    ra=np.zeros(shape=1)+20.7 #specific rubisco activity (umol CO2/g Rub s)
    flnr=np.zeros(shape=1)+0.1 #fraction of leaf nitrogen in rubisco (g N Rub/g N leaf)
    frnr=np.zeros(shape=1)+6.25 #weight fraction of nitrogen in rubisco molecule (g Rub/g N Rub)
    rh=np.zeros(shape=1)+0.5 #relative humidity (kPa/kPa)
    ca=np.zeros(shape=1)+405 #ambient carbon dioxide (umol CO2/mol air)
    ko25=np.zeros(shape=1)+30000 #Michaelis-Menten kinetic coefficient for oxygen at 25 C(Pa)
    kc25=np.zeros(shape=1)+30 #Michaelis-Menten kinetic coefficient for carbon dioxide at 25 C (Pa)
    o=np.zeros(shape=1)+210000 #concentration of ambient oxygen (umol/mol)
    g0=np.zeros(shape=1)+0.002 #Ball-Berry stomatal conductance intercept parameter (mol H2O/m2s)
    a=np.zeros(shape=1)+1.6 #Conversion Coefficient between stomatal conductance to water and carbon dioxide (unitless)
    ij=np.zeros(shape=1)+1.0 #leaf angle index--downregulates jmax
    m=np.zeros(shape=1)+9.0 #ball-berry parameter (unitless)
    b=1.37 #Conversion Coefficient between boundary layer conductance to water and carbon dioxide
    u=5.0 #windspeed (m/s)
    qeff=0.32 #leaf quantum yield, electrons
    PAR=2000 #photosynthetic active radiation (umol/m2s)
    jm=2.68 #slope coefficient
    vwc_min=0.08 #minimum soil water content for photosynthesis to occur (permanent wilting point) (cm3/cm3)
    vwc_max=0.68 #maximum soil water content where increases in soil water do not affect photosynthesis (field capacity?) (cm3/cm3)
    q=0.2 #parameter for soil water affect on photosynthesis (unitless)

    #------constant variable params for sensitivty analysis-----#

    chl_c=np.zeros(shape=1)+(np.mean([396,465,476])) #Chlorophyll Content of the Leaf (umol chl/m2)
    ht_c=np.zeros(shape=1)+10.0 #Temperature of the Leaf (K)
    dia_c=np.zeros(shape=1)+(np.mean([1.4,2.3,2.6])/100.) #Mean diameter or size of leaf (m)
    na_c=np.zeros(shape=1)+(np.mean([2.5,5.6,6.3])) #leaf nitrogen (g N/ m2)
    t_c=np.zeros(shape=1)+15.0 #temp (C)

#---------------Import Variable Parameter Arrays from Leaf Parameter File---------------#
    params=monte_carlo_all()

    for xx in range(len(params)):
        for yy in range(len(params[xx])):
            for key,val in params[xx][yy].items():
                exec(key + '=val')

            #set variable parameters constant if I specify this above
            if 'na' in const_params[ii]:
                na=na_c
            if 'dia' in const_params[ii]:
                dia=dia_c
            if 'chl' in const_params[ii]:
                chl=chl_c
            if 'ht' in const_params[ii]:
                ht=ht_c
            if 't' in const_params[ii]:
                temp=t_c

            #------calculate vapor pressure-----#
            pa_v=611*np.exp((17.27*temp)/(temp+237.3)) #saturation vapor pressure of air (Pa)
            ea_str=pa_con_atmfrac(pa_v,3528) #saturation vapor pressure of air (Pa-->umol h20/mol air)
            ea=rh*ea_str #vapor pressure (umol h2O/mol air)

            #correct for leaf temperatures using leaf height

            t_diff=18-0.4*ht

            tl=temp+t_diff

            if xx==0:

                z=0.2
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_d+=[wue]
                nue_d+=[nue]
                gsw_d+=[gsw]
                A_d+=[A]
                E_d+=[E]
                vpd_d+=[dd]
                tl_d+=[tl]
                gs_d+=[gs]

            elif xx==1:

                z=0.4
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_m+=[wue]

            elif xx==2:

                z=0.4
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_w+=[wue]
                nue_w+=[nue]
                gsw_w+=[gsw]
                A_w+=[A]
                E_w+=[E]
                vpd_w+=[dd]
                tl_w+=[tl]
                gs_w+=[gs]

#---------------Constant WUE---------------#
    params=monte_carlo_all()

    for xx in range(len(params)):
        for yy in range(len(params[xx])):
            for key,val in params[xx][yy].items():
                exec(key + '=val')

            ht=ht_c

            temp=t_c

            #------calculate vapor pressure-----#
            pa_v=611*np.exp((17.27*temp)/(temp+237.3)) #saturation vapor pressure of air (Pa)
            ea_str=pa_con_atmfrac(pa_v,3528) #saturation vapor pressure of air (Pa-->umol h20/mol air)
            ea=rh*ea_str #vapor pressure (umol h2O/mol air)

            #correct for leaf temperatures using leaf height

            t_diff=18-0.4*ht

            tl=temp+t_diff

            if xx==0:

                z=0.2
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_d_const+=[wue]

            if xx==1:

                z=0.2
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_m_const+=[wue]

            elif xx==2:

                z=0.4
                #---------------Photosynthesis Function---------------#

                #alter this line of code for when implementing different photosynthesis functions
                wue, nue, A, E, cs, ci, gsw, gs, gbw, gb, gm, cc,dd =photo(tk_25,ekc,eko,etau,ev,ej,toptv,toptj,na, qeff, PAR,tl,ea,chl,ij,kc25,ko25,o,ca,rh,m,a,frnr,flnr,ra,jm,g0,b,dia,u,q,vwc_min,vwc_max,vwc,z)

                if isinstance(wue, np.ndarray):
                    wue=wue[0]

                if isinstance(nue, np.ndarray):
                    nue=nue[0]

                if isinstance(A, np.ndarray):
                    A=A[0]

                if isinstance(gs, np.ndarray):
                    gs=gs[0]

                if isinstance(gsw, np.ndarray):
                    gsw=gsw[0]

                if isinstance(E, np.ndarray):
                    E=E[0]

                if isinstance(dd, np.ndarray):
                    dd=dd[0]

                if isinstance(wue, list):
                    wue=wue[0]

                if isinstance(nue, list):
                    nue=nue[0]

                if isinstance(A, list):
                    A=A[0]

                if isinstance(gs, list):
                    gs=gs[0]

                if isinstance(gsw, list):
                    gsw=gsw[0]

                if isinstance(E, list):
                    E=E[0]

                if isinstance(dd, list):
                    dd=dd[0]

                wue_w_const+=[wue]

#---------------Figure 1: Plant Communities vs. NUE ---------------#

    #model simulations

    nue_bp=ax1A.boxplot([nue_d,nue_w], patch_artist=True, showmeans=True, showfliers=False)

    ax1A.set_xticks([1.0,2.0])
    ax1A.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman',fontsize=20)

    for box in nue_bp['boxes']:
        #change outline color
        box.set(color='black',linewidth=2)
        #change fill color
        box.set(facecolor='black',alpha=0.2)

    for whisker in nue_bp['whiskers']:
        whisker.set(color='black',linewidth=2)

    for cap in nue_bp['caps']:
        cap.set(color='black',linewidth=2)

    for median in nue_bp['medians']:
        median.set(color='black', linewidth=2)

    for flier in nue_bp['fliers']:
        flier.set(marker='*',color='black',alpha=0.5)

    for means in nue_bp['means']:
        means.set(marker='o',markerfacecolor='black')

    ax1A.annotate('A', xy=get_axis_limits(ax1A,scale1=0.95,scale2=0.88),fontsize=20,fontname="Times New Roman")

    #model validation

    ax1B.bar(ax1A.get_xticks(),[72,88], yerr=[2.08,3.81], edgecolor='black', align="center",width=0.2, color='black',alpha=0.5,error_kw={'ecolor':'black', 'lw':2, 'capsize':5, 'capthick':2})
    ax1B.text(0.97,76,"a",fontsize=20)
    ax1B.text(1.98,93.5,"b",fontsize=20)
    ax1B.annotate('B', xy=get_axis_limits(ax1B,scale2=0.95),fontsize=20,fontname="Times New Roman")
    ax1B.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman',fontsize=25)

#---------------Figure 2: Plant Communities vs. WUE ---------------#

    #model simulations

    wue_bp=ax2A.boxplot([wue_d,wue_w], patch_artist=True, showmeans=True, showfliers=False)

    ax2A.set_xticks([1.0,2.0])
    ax2A.set_xticklabels(['Dry Meadow','Moist Meadow'],fontname='Times New Roman',fontsize=25)

    for box in wue_bp['boxes']:
        #change outline color
        box.set(color='black',linewidth=2)
        #change fill color
        box.set(facecolor='black',alpha=0.2)

    for whisker in wue_bp['whiskers']:
        whisker.set(color='black',linewidth=2)

    for cap in wue_bp['caps']:
        cap.set(color='black',linewidth=2)

    for median in wue_bp['medians']:
        median.set(color='black', linewidth=2)

    for flier in wue_bp['fliers']:
        flier.set(marker='*',color='black',alpha=0.5)

    for means in wue_bp['means']:
        means.set(marker='o',markerfacecolor='black')

    ax2A.annotate('A', xy=get_axis_limits(ax2A,scale1=0.95,scale2=0.85),fontsize=20,fontname="Times New Roman")

    #model validation

    ax2B.bar([1.0,2.0],[1.49,1.57], yerr=[0.06,0.04], edgecolor='black', align="center",width=0.2, color='black',alpha=0.5,error_kw={'ecolor':'black', 'lw':2, 'capsize':5, 'capthick':2})
    ax2B.text(0.97,2,"a",fontsize=20)
    ax2B.text(1.98,2,"a",fontsize=20)
    ax2B.annotate('B', xy=get_axis_limits(ax2B,scale1=0.95,scale2=0.85),fontsize=20,fontname="Times New Roman")
    ax2B.set_xticklabels(['Dry Meadow','Moist Meadow'],fontname='Times New Roman',fontsize=20)

    #model simulations when temp and leaf height are constant
    wue_bp_2=ax2C.boxplot([wue_d_const,wue_m_const], patch_artist=True, showmeans=True, showfliers=False)

    ax2C.set_xticks([1.0,2.0])
    ax2C.set_xticklabels(['Dry Meadow','Moist Meadow'],fontname='Times New Roman',fontsize=30)

    for box in wue_bp_2['boxes']:
        #change outline color
        box.set(color='black',linewidth=2)
        #change fill color
        box.set(facecolor='black',alpha=0.2)

    for whisker in wue_bp_2['whiskers']:
        whisker.set(color='black',linewidth=2)

    for cap in wue_bp_2['caps']:
        cap.set(color='black',linewidth=2)

    for median in wue_bp_2['medians']:
        median.set(color='black', linewidth=2)

    for flier in wue_bp_2['fliers']:
        flier.set(marker='*',color='black',alpha=0.5)

    for means in wue_bp_2['means']:
        means.set(marker='o',markerfacecolor='black')

    ax2C.annotate('C', xy=get_axis_limits(ax2C,scale1=0.95,scale2=0.85),fontsize=20,fontname="Times New Roman")

#---------------Figure 3: Plant Communities vs. Assimilation--------------- #

    A_bp=ax3A.boxplot([A_d,A_w], patch_artist=True, showmeans=True, showfliers=False)

    ax3A.set_xticks([1.0,2.0])
    ax3A.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman')

    #A boxplot specs
    for box in A_bp['boxes']:
        #change outline color
        box.set(color='black',linewidth=2)
        #change fill color
        box.set(facecolor='black',alpha=0.2)

    for whisker in A_bp['whiskers']:
        whisker.set(color='black',linewidth=2)

    for cap in A_bp['caps']:
        cap.set(color='black',linewidth=2)

    for median in A_bp['medians']:
        median.set(color='black', linewidth=2)

    for flier in A_bp['fliers']:
        flier.set(marker='*',color='black',alpha=0.5)

    for means in A_bp['means']:
        means.set(marker='o',markerfacecolor='black')

    ax3A.annotate('A', xy=get_axis_limits(ax3A),fontsize=20,fontname="Times New Roman")

    ax3B.bar([1.0,2.0],[0.91,1.92], yerr=[0.44, 0.81], edgecolor='black', align="center",width=0.2, color='black',alpha=0.5,error_kw={'ecolor':'black', 'lw':2, 'capsize':5, 'capthick':2})
    ax3B.text(0.97,1.6,"a",fontsize=20)
    ax3B.text(1.98,3.0,"b",fontsize=20)
    ax3B.annotate('B', xy=get_axis_limits(ax3B),fontsize=20,fontname="Times New Roman")
    ax3B.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman',fontsize=30)

#---------------Figure 4: leaf height vs. air temperature plot
    ax4.scatter([25,25,25,9,9,5,5,5,5,3,3,3,3,10,10,10,2],[3.6,5.8,12.5,17.2,20.8,13.1,7.2,5.8,11.2,14.,17.7,15.5,24.,14.5,6.3,24.,30.],edgecolors="black",facecolors="black",marker='o',s=30)
    ax4.plot(np.unique([25,25,25,9,9,5,5,5,5,3,3,3,3,10,10,10,2]), np.poly1d(np.polyfit([25,25,25,9,9,5,5,5,5,3,3,3,3,10,10,10,2], [3.6,5.8,12.5,17.2,20.8,13.1,7.2,5.8,11.2,14.,17.7,15.5,24.,14.5,6.3,24.,30.], 1))(np.unique([25,25,25,9,9,5,5,5,5,3,3,3,3,10,10,10,2])),color="black",linewidth=3)

#---------------Figure 5: Regression Plot Assimilation vs. Stomatal Conductance---------------#
    ax5.scatter(A_d+A_w,gs_d+gs_w,edgecolors='black',facecolors='black',marker='o',s=30)
    ax5.plot(np.unique(A_d+A_w), np.poly1d(np.polyfit(A_d+A_w, gs_d+gs_w, 1))(np.unique(A_d+A_w)),color='black',linewidth=3)

#---------------Figure 6: Plot VPD vs. WUE for validation (use all points rather than mean of points)-------------#
    def func(x, a, b, c):
        return a * np.exp(-b * x) + c
    xdata=np.array(vpd_d+vpd_w)/10000.
    ydata=np.array(wue_d+wue_w)
    L=sorted(zip(xdata,ydata),key=operator.itemgetter(0))
    new_x,new_y=zip(*L)
    popt, pcov = curve_fit(func, np.array(new_x), np.array(new_y))

    ax6B.plot(np.array(new_x), func(np.array(new_x), *popt), 'k-', linewidth=3,label='Model Simulation')
    ax6B.scatter(xdata,ydata,edgecolors='black',facecolors='black',marker='o',s=30)
    ax6B.annotate('B', xy=get_axis_limits(ax6B),fontsize=20,fontname="Times New Roman")

    def func(x, a, b, c):
        return a * np.exp(-b * x) + c
    xdata=np.array([13734.61196533664,13734.61196533664,13734.61196533664,13734.61196533664,21364.95194607922,21364.95194607922,21364.95194607922,21364.95194607922,28995.2919268218,28995.2919268218,28995.2919268218,28995.2919268218,13734.61196533664,13734.61196533664,13734.61196533664,13734.61196533664,21364.95194607922,21364.95194607922,21364.95194607922,21364.95194607922,28995.2919268218,28995.2919268218,28995.2919268218,28995.2919268218])/10000.
    ydata=np.array([11,12,11,3,5,4.5,1,5,4,2,1,.5,9,9,10,11,5,5,5,5,5,5,5,2])*.4091
    L=sorted(zip(xdata,ydata),key=operator.itemgetter(0))
    new_x,new_y=zip(*L)
    popt, pcov = curve_fit(func, np.array(new_x), np.array(new_y))

    ax6A.plot(np.array(new_x), func(np.array(new_x), *popt), 'k-', linewidth=3,label='fit')
    ax6A.scatter(xdata,ydata,edgecolors="black",facecolors="black",marker='o',s=30,label='Empirical Data')
    ax6A.annotate('A', xy=get_axis_limits(ax6A),fontsize=20,fontname="Times New Roman")

#-----------FINALIZE PLOT------#

    figs=[fig1,fig2,fig3,fig4,fig5,fig6]
    axes=[ax1A,ax2A,ax3A,ax1B,ax2B,ax3B,ax2C,ax4,ax5,ax6A,ax6B]

    for i in range(len(axes)):
        axes[i].tick_params(axis='y', labelsize=15)
        axes[i].tick_params(axis='x', labelsize=15)
        for tick in axes[i].get_xticklabels():
            tick.set_fontname("Times New Roman")
        for tick in axes[i].get_yticklabels():
            tick.set_fontname("Times New Roman")

    ax1B.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman',fontsize=20)
    ax2C.set_xticklabels(['Dry Meadow','Moist Meadow'],fontname='Times New Roman',fontsize=20)
    ax3B.set_xticklabels(['Dry Meadow','Wet Meadow'],fontname='Times New Roman',fontsize=20)

#    for i in range(len(figs)):
#        figs[i].tight_layout()

#---------------Finalize Figure---------------#

    ##---Save Figure--##
    fig1.savefig('NUE_val.png')
    fig2.savefig('WUE_val.png')
    fig3.savefig('Assimilation_val.png')
    fig4.savefig('Leaf_Ht_Temp.png')
    fig5.savefig('Assimilation_vs_Conductance.png')
    fig6.savefig('VPD_vs_WUE.png')


import numpy as np
import pandas as pd

from pvlib.tools import cosd, sind, tand
from pvlib.pvsystem import _combine_localized_attributes
from pvlib.pvsystem import PVSystem
from pvlib.location import Location
from pvlib import irradiance, atmosphere
from pvlib._deprecation import deprecated

class SingleAxisTracker(PVSystem):
    """
    A class for single-axis trackers that inherits the PV modeling methods from
    :py:class:`~pvlib.pvsystem.PVSystem`. For details on calculating tracker
    rotation see :py:func:`pvlib.tracking.singleaxis`.

    Parameters
    ----------
    axis_tilt : float, default 0
        The tilt of the axis of rotation (i.e, the y-axis defined by
        axis_azimuth) with respect to horizontal, in decimal degrees.

    axis_azimuth : float, default 0
        A value denoting the compass direction along which the axis of
        rotation lies. Measured in decimal degrees east of north.

    max_angle : float, default 90
        A value denoting the maximum rotation angle, in decimal degrees,
        of the one-axis tracker from its horizontal position (horizontal
        if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
        to rotate to a vertical position to point the panel towards a
        horizon. max_angle of 180 degrees allows for full rotation.

    backtrack : bool, default True
        Controls whether the tracker has the capability to "backtrack"
        to avoid row-to-row shading. False denotes no backtrack
        capability. True denotes backtrack capability.

    gcr : float, default 2.0/7.0
        A value denoting the ground coverage ratio of a tracker system
        which utilizes backtracking; i.e. the ratio between the PV array
        surface area to total ground area. A tracker system with modules
        2 meters wide, centered on the tracking axis, with 6 meters
        between the tracking axes has a gcr of 2/6=0.333. If gcr is not
        provided, a gcr of 2/7 is default. gcr must be <=1.

    cross_axis_tilt : float, default 0.0
        The angle, relative to horizontal, of the line formed by the
        intersection between the slope containing the tracker axes and a plane
        perpendicular to the tracker axes. Cross-axis tilt should be specified
        using a right-handed convention. For example, trackers with axis
        azimuth of 180 degrees (heading south) will have a negative cross-axis
        tilt if the tracker axes plane slopes down to the east and positive
        cross-axis tilt if the tracker axes plane slopes up to the east. Use
        :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
        `cross_axis_tilt`. [degrees]

    **kwargs
        Passed to :py:class:`~pvlib.pvsystem.PVSystem`.

    See also
    --------
    pvlib.tracking.singleaxis
    pvlib.tracking.calc_axis_tilt
    pvlib.tracking.calc_cross_axis_tilt
    """

    def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90,
                 backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs):

        self.axis_tilt = axis_tilt
        self.axis_azimuth = axis_azimuth
        self.max_angle = max_angle
        self.backtrack = backtrack
        self.gcr = gcr
        self.cross_axis_tilt = cross_axis_tilt

        kwargs['surface_tilt'] = None
        kwargs['surface_azimuth'] = None

        super().__init__(**kwargs)

    def __repr__(self):
        attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr',
                 'cross_axis_tilt']
        sat_repr = ('SingleAxisTracker:\n  ' + '\n  '.join(
            f'{attr}: {getattr(self, attr)}' for attr in attrs))
        # get the parent PVSystem info
        pvsystem_repr = super().__repr__()
        # remove the first line (contains 'PVSystem: \n')
        pvsystem_repr = '\n'.join(pvsystem_repr.split('\n')[1:])
        return sat_repr + '\n' + pvsystem_repr

    def singleaxis(self, apparent_zenith, apparent_azimuth):
        """
        Get tracking data. See :py:func:`pvlib.tracking.singleaxis` more
        detail.

        Parameters
        ----------
        apparent_zenith : float, 1d array, or Series
            Solar apparent zenith angles in decimal degrees.

        apparent_azimuth : float, 1d array, or Series
            Solar apparent azimuth angles in decimal degrees.

        Returns
        -------
        tracking data
        """
        tracking_data = singleaxis(apparent_zenith, apparent_azimuth,
                                   self.axis_tilt, self.axis_azimuth,
                                   self.max_angle, self.backtrack,
                                   self.gcr, self.cross_axis_tilt)

        return tracking_data

    @deprecated('0.8',
                alternative='SingleAxisTracker, Location, and ModelChain',
                name='SingleAxisTracker.localize', removal='0.9')
    def localize(self, location=None, latitude=None, longitude=None,
                 **kwargs):
        """
        Creates a :py:class:`LocalizedSingleAxisTracker` object using
        this object and location data. Must supply either location
        object or latitude, longitude, and any location kwargs

        Parameters
        ----------
        location : None or Location, default None
        latitude : None or float, default None
        longitude : None or float, default None
        **kwargs : see Location

        Returns
        -------
        localized_system : LocalizedSingleAxisTracker
        """

        if location is None:
            location = Location(latitude, longitude, **kwargs)

        return LocalizedSingleAxisTracker(pvsystem=self, location=location)

    def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith,
                solar_azimuth):
        """Get the angle of incidence on the system.

        For a given set of solar zenith and azimuth angles, the
        surface tilt and azimuth parameters are typically determined
        by :py:method:`~SingleAxisTracker.singleaxis`. The
        :py:method:`~SingleAxisTracker.singleaxis` method also returns
        the angle of incidence, so this method is only needed
        if using a different tracking algorithm.

        Parameters
        ----------
        surface_tilt : numeric
            Panel tilt from horizontal.
        surface_azimuth : numeric
            Panel azimuth from north
        solar_zenith : float or Series.
            Solar zenith angle.
        solar_azimuth : float or Series.
            Solar azimuth angle.

        Returns
        -------
        aoi : Series
            The angle of incidence in degrees from normal.
        """

        aoi = irradiance.aoi(surface_tilt, surface_azimuth,
                             solar_zenith, solar_azimuth)
        return aoi

    def get_irradiance(self, surface_tilt, surface_azimuth,
                       solar_zenith, solar_azimuth, dni, ghi, dhi,
                       dni_extra=None, airmass=None, model='haydavies',
                       **kwargs):
        """
        Uses the :func:`irradiance.get_total_irradiance` function to
        calculate the plane of array irradiance components on a tilted
        surface defined by the input data and ``self.albedo``.

        For a given set of solar zenith and azimuth angles, the
        surface tilt and azimuth parameters are typically determined
        by :py:meth:`~SingleAxisTracker.singleaxis`.

        Parameters
        ----------
        surface_tilt : numeric
            Panel tilt from horizontal.
        surface_azimuth : numeric
            Panel azimuth from north
        solar_zenith : numeric
            Solar zenith angle.
        solar_azimuth : numeric
            Solar azimuth angle.
        dni : float or Series
            Direct Normal Irradiance
        ghi : float or Series
            Global horizontal irradiance
        dhi : float or Series
            Diffuse horizontal irradiance
        dni_extra : float or Series, default None
            Extraterrestrial direct normal irradiance
        airmass : float or Series, default None
            Airmass
        model : String, default 'haydavies'
            Irradiance model.

        **kwargs
            Passed to :func:`irradiance.get_total_irradiance`.

        Returns
        -------
        poa_irradiance : DataFrame
            Column names are: ``total, beam, sky, ground``.
        """

        # not needed for all models, but this is easier
        if dni_extra is None:
            dni_extra = irradiance.get_extra_radiation(solar_zenith.index)

        if airmass is None:
            airmass = atmosphere.get_relative_airmass(solar_zenith)

        return irradiance.get_total_irradiance(surface_tilt,
                                               surface_azimuth,
                                               solar_zenith,
                                               solar_azimuth,
                                               dni, ghi, dhi,
                                               dni_extra=dni_extra,
                                               airmass=airmass,
                                               model=model,
                                               albedo=self.albedo,
                                               **kwargs)

@deprecated('0.8', alternative='SingleAxisTracker, Location, and ModelChain',
            name='LocalizedSingleAxisTracker', removal='0.9')
class LocalizedSingleAxisTracker(SingleAxisTracker, Location):
    """
    The :py:class:`~pvlib.tracking.LocalizedSingleAxisTracker` class defines a
    standard set of installed PV system attributes and modeling functions. This
    class combines the attributes and methods of the
    :py:class:`~pvlib.tracking.SingleAxisTracker` (a subclass of
    :py:class:`~pvlib.pvsystem.PVSystem`) and
    :py:class:`~pvlib.location.Location` classes.

    The :py:class:`~pvlib.tracking.LocalizedSingleAxisTracker` may have bugs
    due to the difficulty of robustly implementing multiple inheritance. See
    :py:class:`~pvlib.modelchain.ModelChain` for an alternative paradigm
    for modeling PV systems at specific locations.
    """

    def __init__(self, pvsystem=None, location=None, **kwargs):

        new_kwargs = _combine_localized_attributes(
            pvsystem=pvsystem,
            location=location,
            **kwargs,
        )

        SingleAxisTracker.__init__(self, **new_kwargs)
        Location.__init__(self, **new_kwargs)

    def __repr__(self):
        attrs = ['latitude', 'longitude', 'altitude', 'tz']
        return ('Localized' +
                super().__repr__() + '\n  ' +
                '\n  '.join(
                    f'{attr}: {getattr(self, attr)}' for attr in attrs))

def singleaxis(apparent_zenith, apparent_azimuth,
               axis_tilt=0, axis_azimuth=0, max_angle=90,
               backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0):
    """
    Determine the rotation angle of a single-axis tracker when given particular
    solar zenith and azimuth angles.

    See [1]_ for details about the equations. Backtracking may be specified,
    and if so, a ground coverage ratio is required.

    Rotation angle is determined in a right-handed coordinate system. The
    tracker `axis_azimuth` defines the positive y-axis, the positive x-axis is
    90 degrees clockwise from the y-axis and parallel to the Earth's surface,
    and the positive z-axis is normal to both x & y-axes and oriented skyward.
    Rotation angle `tracker_theta` is a right-handed rotation around the y-axis
    in the x, y, z coordinate system and indicates tracker position relative to
    horizontal. For example, if tracker `axis_azimuth` is 180 (oriented south)
    and `axis_tilt` is zero, then a `tracker_theta` of zero is horizontal, a
    `tracker_theta` of 30 degrees is a rotation of 30 degrees towards the west,
    and a `tracker_theta` of -90 degrees is a rotation to the vertical plane
    facing east.

    Parameters
    ----------
    apparent_zenith : float, 1d array, or Series
        Solar apparent zenith angles in decimal degrees.

    apparent_azimuth : float, 1d array, or Series
        Solar apparent azimuth angles in decimal degrees.

    axis_tilt : float, default 0
        The tilt of the axis of rotation (i.e, the y-axis defined by
        axis_azimuth) with respect to horizontal, in decimal degrees.

    axis_azimuth : float, default 0
        A value denoting the compass direction along which the axis of
        rotation lies. Measured in decimal degrees east of north.

    max_angle : float, default 90
        A value denoting the maximum rotation angle, in decimal degrees,
        of the one-axis tracker from its horizontal position (horizontal
        if axis_tilt = 0). A max_angle of 90 degrees allows the tracker
        to rotate to a vertical position to point the panel towards a
        horizon. max_angle of 180 degrees allows for full rotation.

    backtrack : bool, default True
        Controls whether the tracker has the capability to "backtrack"
        to avoid row-to-row shading. False denotes no backtrack
        capability. True denotes backtrack capability.

    gcr : float, default 2.0/7.0
        A value denoting the ground coverage ratio of a tracker system
        which utilizes backtracking; i.e. the ratio between the PV array
        surface area to total ground area. A tracker system with modules
        2 meters wide, centered on the tracking axis, with 6 meters
        between the tracking axes has a gcr of 2/6=0.333. If gcr is not
        provided, a gcr of 2/7 is default. gcr must be <=1.

    cross_axis_tilt : float, default 0.0
        The angle, relative to horizontal, of the line formed by the
        intersection between the slope containing the tracker axes and a plane
        perpendicular to the tracker axes. Cross-axis tilt should be specified
        using a right-handed convention. For example, trackers with axis
        azimuth of 180 degrees (heading south) will have a negative cross-axis
        tilt if the tracker axes plane slopes down to the east and positive
        cross-axis tilt if the tracker axes plane slopes up to the east. Use
        :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
        `cross_axis_tilt`. [degrees]

    Returns
    -------
    dict or DataFrame with the following columns:
        * `tracker_theta`: The rotation angle of the tracker.
          tracker_theta = 0 is horizontal, and positive rotation angles are
          clockwise. [degrees]
        * `aoi`: The angle-of-incidence of direct irradiance onto the
          rotated panel surface. [degrees]
        * `surface_tilt`: The angle between the panel surface and the earth
          surface, accounting for panel rotation. [degrees]
        * `surface_azimuth`: The azimuth of the rotated panel, determined by
          projecting the vector normal to the panel's surface to the earth's
          surface. [degrees]

    See also
    --------
    pvlib.tracking.calc_axis_tilt
    pvlib.tracking.calc_cross_axis_tilt

    References
    ----------
    .. [1] Kevin Anderson and Mark Mikofski, "Slope-Aware Backtracking for
       Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020.
       https://www.nrel.gov/docs/fy20osti/76626.pdf
    """

    # MATLAB to Python conversion by
    # Will Holmgren (@wholmgren), U. Arizona. March, 2015.

    if isinstance(apparent_zenith, pd.Series):
        index = apparent_zenith.index
    else:
        index = None

    # convert scalars to arrays
    apparent_azimuth = np.atleast_1d(apparent_azimuth)
    apparent_zenith = np.atleast_1d(apparent_zenith)

    if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1:
        raise ValueError('Input dimensions must not exceed 1')

    # Calculate sun position x, y, z using coordinate system as in [1], Eq 1.

    # NOTE: solar elevation = 90 - solar zenith, then use trig identities:
    # sin(90-x) = cos(x) & cos(90-x) = sin(x)
    sin_zenith = sind(apparent_zenith)
    x = sin_zenith * sind(apparent_azimuth)
    y = sin_zenith * cosd(apparent_azimuth)
    z = cosd(apparent_zenith)

    # Assume the tracker reference frame is right-handed. Positive y-axis is
    # oriented along tracking axis; from north, the y-axis is rotated clockwise
    # by the axis azimuth and tilted from horizontal by the axis tilt. The
    # positive x-axis is 90 deg clockwise from the y-axis and parallel to
    # horizontal (e.g., if the y-axis is south, the x-axis is west); the
    # positive z-axis is normal to the x and y axes, pointed upward.

    # Calculate sun position (xp, yp, zp) in tracker coordinate system using
    # [1] Eq 4.

    cos_axis_azimuth = cosd(axis_azimuth)
    sin_axis_azimuth = sind(axis_azimuth)
    cos_axis_tilt = cosd(axis_tilt)
    sin_axis_tilt = sind(axis_tilt)
    xp = x*cos_axis_azimuth - y*sin_axis_azimuth
    yp = (x*cos_axis_tilt*sin_axis_azimuth
          + y*cos_axis_tilt*cos_axis_azimuth
          - z*sin_axis_tilt)
    zp = (x*sin_axis_tilt*sin_axis_azimuth
          + y*sin_axis_tilt*cos_axis_azimuth
          + z*cos_axis_tilt)

    # The ideal tracking angle wid is the rotation to place the sun position
    # vector (xp, yp, zp) in the (y, z) plane, which is normal to the panel and
    # contains the axis of rotation.  wid = 0 indicates that the panel is
    # horizontal. Here, our convention is that a clockwise rotation is
    # positive, to view rotation angles in the same frame of reference as
    # azimuth. For example, for a system with tracking axis oriented south, a
    # rotation toward the east is negative, and a rotation to the west is
    # positive. This is a right-handed rotation around the tracker y-axis.

    # Calculate angle from x-y plane to projection of sun vector onto x-z plane
    # using [1] Eq. 5.

    wid = np.degrees(np.arctan2(xp, zp))

    # filter for sun above panel horizon
    zen_gt_90 = apparent_zenith > 90
    wid[zen_gt_90] = np.nan

    # Account for backtracking
    if backtrack:
        # distance between rows in terms of rack lengths relative to cross-axis
        # tilt
        axes_distance = 1/(gcr * cosd(cross_axis_tilt))

        # NOTE: account for rare angles below array, see GH 824
        temp = np.abs(axes_distance * cosd(wid - cross_axis_tilt))

        # backtrack angle using [1], Eq. 14
        with np.errstate(invalid='ignore'):
            wc = np.degrees(-np.sign(wid)*np.arccos(temp))

        # NOTE: in the middle of the day, arccos(temp) is out of range because
        # there's no row-to-row shade to avoid, & backtracking is unnecessary
        # [1], Eqs. 15-16
        with np.errstate(invalid='ignore'):
            tracker_theta = wid + np.where(temp < 1, wc, 0)
    else:
        tracker_theta = wid

    # NOTE: max_angle defined relative to zero-point rotation, not the
    # system-plane normal
    tracker_theta = np.clip(tracker_theta, -max_angle, max_angle)

    # Calculate panel normal vector in panel-oriented x, y, z coordinates.
    # y-axis is axis of tracker rotation. tracker_theta is a compass angle
    # (clockwise is positive) rather than a trigonometric angle.
    # NOTE: the *0 is a trick to preserve NaN values.
    panel_norm = np.array([sind(tracker_theta),
                           tracker_theta*0,
                           cosd(tracker_theta)])

    # sun position in vector format in panel-oriented x, y, z coordinates
    sun_vec = np.array([xp, yp, zp])

    # calculate angle-of-incidence on panel
    aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec*panel_norm, axis=0))))

    # Calculate panel tilt and azimuth in a coordinate system where the panel
    # tilt is the angle from horizontal, and the panel azimuth is the compass
    # angle (clockwise from north) to the projection of the panel's normal to
    # the earth's surface. These outputs are provided for convenience and
    # comparison with other PV software which use these angle conventions.

    # Project normal vector to earth surface. First rotate about x-axis by
    # angle -axis_tilt so that y-axis is also parallel to earth surface, then
    # project.

    # Calculate standard rotation matrix
    rot_x = np.array([[1, 0, 0],
                      [0, cosd(-axis_tilt), -sind(-axis_tilt)],
                      [0, sind(-axis_tilt), cosd(-axis_tilt)]])

    # panel_norm_earth contains the normal vector expressed in earth-surface
    # coordinates (z normal to surface, y aligned with tracker axis parallel to
    # earth)
    panel_norm_earth = np.dot(rot_x, panel_norm).T

    # projection to plane tangent to earth surface, in earth surface
    # coordinates
    projected_normal = np.array([panel_norm_earth[:, 0],
                                 panel_norm_earth[:, 1],
                                 panel_norm_earth[:, 2]*0]).T

    # calculate vector magnitudes
    projected_normal_mag = np.sqrt(np.nansum(projected_normal**2, axis=1))

    # renormalize the projected vector, avoid creating nan values.
    non_zeros = projected_normal_mag != 0
    projected_normal[non_zeros] = (projected_normal[non_zeros].T /
                                   projected_normal_mag[non_zeros]).T

    # calculation of surface_azimuth
    surface_azimuth = \
        np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0]))

    # Rotate 0 reference from panel's x-axis to its y-axis and then back to
    # north.
    surface_azimuth = 90 - surface_azimuth + axis_azimuth

    # Map azimuth into [0,360) domain.
    with np.errstate(invalid='ignore'):
        surface_azimuth = surface_azimuth % 360

    # Calculate surface_tilt
    dotproduct = (panel_norm_earth * projected_normal).sum(axis=1)
    surface_tilt = 90 - np.degrees(np.arccos(dotproduct))

    # Bundle DataFrame for return values and filter for sun below horizon.
    out = {'tracker_theta': tracker_theta, 'aoi': aoi,
           'surface_tilt': surface_tilt, 'surface_azimuth': surface_azimuth}
    if index is not None:
        out = pd.DataFrame(out, index=index)
        out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']]
        out[zen_gt_90] = np.nan
    else:
        out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()}

    return out

def calc_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth):
    """
    Calculate tracker axis tilt in the global reference frame when on a sloped
    plane.

    Parameters
    ----------
    slope_azimuth : float
        direction of normal to slope on horizontal [degrees]
    slope_tilt : float
        tilt of normal to slope relative to vertical [degrees]
    axis_azimuth : float
        direction of tracker axes on horizontal [degrees]

    Returns
    -------
    axis_tilt : float
        tilt of tracker [degrees]

    See also
    --------
    pvlib.tracking.singleaxis
    pvlib.tracking.calc_cross_axis_tilt

    Notes
    -----
    See [1]_ for derivation of equations.

    References
    ----------
    .. [1] Kevin Anderson and Mark Mikofski, "Slope-Aware Backtracking for
       Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020.
       https://www.nrel.gov/docs/fy20osti/76626.pdf
    """
    delta_gamma = axis_azimuth - slope_azimuth
    # equations 18-19
    tan_axis_tilt = cosd(delta_gamma) * tand(slope_tilt)
    return np.degrees(np.arctan(tan_axis_tilt))

def _calc_tracker_norm(ba, bg, dg):
    """
    Calculate tracker normal, v, cross product of tracker axis and unit normal,
    N, to the system slope plane.

    Parameters
    ----------
    ba : float
        axis tilt [degrees]
    bg : float
        ground tilt [degrees]
    dg : float
        delta gamma, difference between axis and ground azimuths [degrees]

    Returns
    -------
    vector : tuple
        vx, vy, vz
    """
    cos_ba = cosd(ba)
    cos_bg = cosd(bg)
    sin_bg = sind(bg)
    sin_dg = sind(dg)
    vx = sin_dg * cos_ba * cos_bg
    vy = sind(ba)*sin_bg + cosd(dg)*cos_ba*cos_bg
    vz = -sin_dg*sin_bg*cos_ba
    return vx, vy, vz

def _calc_beta_c(v, dg, ba):
    """
    Calculate the cross-axis tilt angle.

    Parameters
    ----------
    v : tuple
        tracker normal
    dg : float
        delta gamma, difference between axis and ground azimuths [degrees]
    ba : float
        axis tilt [degrees]

    Returns
    -------
    beta_c : float
        cross-axis tilt angle [radians]
    """
    vnorm = np.sqrt(np.dot(v, v))
    beta_c = np.arcsin(
        ((v[0]*cosd(dg) - v[1]*sind(dg)) * sind(ba) + v[2]*cosd(ba)) / vnorm)
    return beta_c

def calc_cross_axis_tilt(
        slope_azimuth, slope_tilt, axis_azimuth, axis_tilt):
    """
    Calculate the angle, relative to horizontal, of the line formed by the
    intersection between the slope containing the tracker axes and a plane
    perpendicular to the tracker axes.

    Use the cross-axis tilt to avoid row-to-row shade when backtracking on a
    slope not parallel with the axis azimuth. Cross-axis tilt should be
    specified using a right-handed convention. For example, trackers with axis
    azimuth of 180 degrees (heading south) will have a negative cross-axis tilt
    if the tracker axes plane slopes down to the east and positive cross-axis
    tilt if the tracker axes plane slopes up to the east.

    Parameters
    ----------
    slope_azimuth : float
        direction of the normal to the slope containing the tracker axes, when
        projected on the horizontal [degrees]
    slope_tilt : float
        angle of the slope containing the tracker axes, relative to horizontal
        [degrees]
    axis_azimuth : float
        direction of tracker axes projected on the horizontal [degrees]
    axis_tilt : float
        tilt of trackers relative to horizontal [degrees]

    Returns
    -------
    cross_axis_tilt : float
        angle, relative to horizontal, of the line formed by the intersection
        between the slope containing the tracker axes and a plane perpendicular
        to the tracker axes [degrees]

    See also
    --------
    pvlib.tracking.singleaxis
    pvlib.tracking.calc_axis_tilt

    Notes
    -----
    See [1]_ for derivation of equations.

    References
    ----------
    .. [1] Kevin Anderson and Mark Mikofski, "Slope-Aware Backtracking for
       Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020.
       https://www.nrel.gov/docs/fy20osti/76626.pdf
    """
    # delta-gamma, difference between axis and slope azimuths
    delta_gamma = axis_azimuth - slope_azimuth
    # equation 22
    v = _calc_tracker_norm(axis_tilt, slope_tilt, delta_gamma)
    # equation 26
    beta_c = _calc_beta_c(v, delta_gamma, axis_tilt)
    return np.degrees(beta_c)


# FIXME: make pylint happy !
#pylint: disable=all

from sympy.utilities.codegen import CodeGen, CodeGenError, ResultBase, Result, InputArgument, InOutArgument, OutputArgument
from sympy.core import Symbol, S, Expr, Tuple, Equality, Function, sympify
from sympy.core.compatibility import is_sequence, StringIO, string_types
from sympy.printing.codeprinter import AssignmentError
from sympy.core.sympify import _sympify, sympify

from sympy.tensor import Idx, Indexed, IndexedBase
from sympy.matrices import (MatrixSymbol, ImmutableMatrix, MatrixBase,
                            MatrixExpr, MatrixSlice)
from sympy.core.basic import Basic

default_settings = {'export': True}

from .ast import For, If
from .printing.cython import cython_code, CythonCodePrinter
from .printing.numpy import numpy_code, NumpyCodePrinter
from .printing.loopy import loopy_code, LoopyCodePrinter

class Routine(object):
    """Generic description of evaluation routine for set of expressions.

    A CodeGen class can translate instances of this class into code in a
    particular language.  The routine specification covers all the features
    present in these languages.  The CodeGen part must raise an exception
    when certain features are not present in the target language.  For
    example, multiple return values are possible in Python, but not in C or
    Fortran.  Another example: Fortran and Python support complex numbers,
    while C does not.

    """

    def __init__(self, name, arguments, instructions, idx_vars, local_vars, settings={}):
        """Initialize a Routine instance.

        Parameters
        ==========

        name : string
            Name of the routine.

        arguments : list of Arguments
            These are things that appear in arguments of a routine, often
            appearing on the right-hand side of a function call.  These are
            commonly InputArguments but in some languages, they can also be
            OutputArguments or InOutArguments (e.g., pass-by-reference in C
            code).

        instructions : list
            Instructions of the routine.

        local_vars : list of Symbols
            These are used internally by the routine.

        global_vars : list of Symbols
            Variables which will not be passed into the function.

        """

        # extract all input symbols and all symbols appearing in an expression
        input_symbols = set([])
        symbols = set([])
        for arg in arguments:
            if isinstance(arg, OutputArgument):
                symbols.update(arg.expr.free_symbols)
            elif isinstance(arg, InputArgument):
                input_symbols.add(arg.name)
            elif isinstance(arg, InOutArgument):
                input_symbols.add(arg.name)
                symbols.update(arg.expr.free_symbols)
            else:
                raise ValueError("Unknown Routine argument: %s" % arg)

        for i in instructions:
            symbols.update(i.free_symbols)

        symbols = set([s.label if isinstance(s, Idx) else s for s in symbols])

        # Check that all symbols in the expressions are covered by
        # InputArguments/InOutArguments---subset because user could
        # specify additional (unused) InputArguments or local_vars.
        dummy = [i.label for i in idx_vars]
        symbol_indexed_local = set()
        for l in local_vars:
            for ll in l.atoms(IndexedBase):
                symbol_indexed_local.update(ll.atoms(Symbol) - ll.shape.atoms(Symbol))

        notcovered = symbols.difference(
            input_symbols.union(dummy).union(local_vars).union(symbol_indexed_local))
        if notcovered != set([]):
            raise ValueError("Symbols needed for output are not in input " +
                             ", ".join([str(x) for x in notcovered]))

        self.name = name
        self.arguments = arguments
        self.instructions = instructions
        self.idx_vars = idx_vars
        self.local_vars = local_vars
        self.settings = settings

    def __str__(self):
        return self.__class__.__name__ + "({name!r}, {arguments}, {instructions}, {idx_vars}, {local_vars})".format(**self.__dict__)

    __repr__ = __str__

    @property
    def variables(self):
        """Returns a set of all variables possibly used in the routine.

        For routines with unnamed return values, the dummies that may or
        may not be used will be included in the set.

        """
        v = set(self.local_vars)
        for arg in self.arguments:
            v.add(arg.name)
        for res in self.results:
            v.add(res.result_var)
        return v

def get_dims_and_symbol(expr):
    if isinstance(expr, Indexed):
        dims = tuple([ (S.Zero, dim - 1) for dim in expr.shape])
        symbol = expr.base.label
    elif isinstance(expr, Symbol):
        dims = []
        symbol = expr
    elif isinstance(expr, MatrixSymbol):
        dims = tuple([ (S.Zero, dim - 1) for dim in expr.shape if dim != 1])
        symbol = expr
    elif isinstance(expr, MatrixBase):
        # if we have a Matrix, we set line by line the code
        # useful when you have indexes like
        # A[i, j, 0] = B[i, j, 4]

        # todo: regarder que les matrices pour le in et out ont la meme taille
        # todo: regarder si le symbole est toujours le meme dans le terme de gauche (expr)
        for i in range(expr.shape[0]):
            symbol = expr[i].base.label
            dims = tuple([ (S.Zero, dim - 1) for dim in expr[i].base.shape if dim != 1])
    elif isinstance(expr, MatrixSlice):
        symbol = expr.parent
        dims = tuple([ (S.Zero, dim - 1) for dim in symbol.shape if dim != 1])
    else:
        raise CodeGenError("Only Indexed, Symbol, or MatrixSymbol "
                            "can define output arguments.")
    return dims, symbol

def extract(expressions, symbols):
    # extract arguments and instructions of the routine
    output_args = []
    instructions = []
    for expr in expressions:
        instructions.append(expr)
        if isinstance(expr, Equality):
            out_arg = expr.lhs
            expr = expr.rhs
            dims, symbol = get_dims_and_symbol(out_arg)

            if symbol in symbols:
                output_args.append(
                    InOutArgument(symbol, out_arg, expr, dimensions=dims))

                # avoid duplicate arguments
                symbols.remove(symbol)
        elif isinstance(expr, For):
            args, vals = extract(expr.expr, symbols)
            output_args += args
        elif isinstance(expr, If):
            for c, e in expr.statement:
                args, vals = extract(e, symbols)
                output_args += args
        else:
            raise TypeError("The expression must be a For or an equality (Eq).")
    return output_args, instructions

class LBMCodeGen(CodeGen):
    """Generator for Cython code.

    The .write() method inherited from CodeGen will output a code file <prefix>.pyx.

    """

    code_extension = None

    def routine(self, name, expr, argument_sequence, local_vars, settings):
        """Specialized Routine creation for Cython."""

        if is_sequence(expr) and not isinstance(expr, (MatrixBase, MatrixExpr)):
            if not expr:
                raise ValueError("No expression given")
            expressions = Tuple(*expr)
        else:
            expressions = Tuple(expr)

        self.settings = settings

        # local variables
        idx_vars = set()
        symbol_idx_vars = set()
        for l in expressions.atoms(For):
            idx_vars.update({i for i in l.atoms(Idx)})
            # remove symbols that have the same name of Idx in loop
            name_idx = [i.label.name for i in l.atoms(Idx)]
            symbol_idx_vars.update({i for i in l.atoms(Symbol) if i.name in name_idx})

        score_table = {}
        for i in idx_vars:
            score_table[i] = 0

        def rate_index_position(p):
            return p*5

        arrays = expressions.atoms(Indexed)
        for arr in arrays:
            for p, ind in enumerate(arr.indices):
                try:
                    score_table[ind] += rate_index_position(p)
                except KeyError:
                    pass

        idx_order = sorted(idx_vars, key=lambda x: score_table[x])

        # local variables
        local_vars = set() if local_vars is None else set(local_vars)

        # symbols that should be arguments
        symbol_indexed_local = set()
        for l in local_vars:
            for ll in l.atoms(IndexedBase):
                symbol_indexed_local.update(ll.atoms(Symbol) - ll.shape.atoms(Symbol))

        symbols = expressions.free_symbols - idx_vars - local_vars - symbol_idx_vars - symbol_indexed_local

        new_symbols = set([])
        new_symbols.update(symbols)

        for symbol in symbols:
            if isinstance(symbol, Idx):
                new_symbols.remove(symbol)
                if symbol.label in idx_vars:
                    new_symbols.update(symbol.args[1].free_symbols)
                else:
                    new_symbols.update([symbol.label])
        symbols = new_symbols

        output_args, instructions = extract(expressions, symbols)

        arg_list = []

        # setup input argument list
        array_symbols = {}
        for array in expressions.atoms(Indexed):
            array_symbols[array.base.label] = array
        for array in expressions.atoms(MatrixSymbol):
            array_symbols[array] = array

        for symbol in sorted(symbols, key=str):
            if symbol in array_symbols:
                dims = []
                array = array_symbols[symbol]
                for dim in array.shape:
                    if dim != 1:
                        dims.append((S.Zero, dim - 1))
                metadata = {'dimensions': dims}
            else:
                metadata = {}

            arg_list.append(InputArgument(symbol, **metadata))

        output_args.sort(key=lambda x: str(x.name))
        arg_list.extend(output_args)

        if argument_sequence is not None:
            # if the user has supplied IndexedBase instances, we'll accept that
            new_sequence = []
            for arg in argument_sequence:
                if isinstance(arg, IndexedBase):
                    new_sequence.append(arg.label)
                else:
                    new_sequence.append(arg)
            argument_sequence = new_sequence

            missing = [x for x in arg_list if x.name not in argument_sequence]
            if missing:
                msg = "Argument list didn't specify: {0} "
                msg = msg.format(", ".join([str(m.name) for m in missing]))
                raise CodeGenArgumentListError(msg, missing)

            # create redundant arguments to produce the requested sequence
            name_arg_dict = {x.name: x for x in arg_list}
            new_args = []
            for symbol in argument_sequence:
                try:
                    new_args.append(name_arg_dict[symbol])
                except KeyError:
                    new_args.append(InputArgument(symbol))
            arg_list = new_args

        return Routine(name, arg_list, instructions, idx_order, local_vars, settings)

    def code_generator(self, expr, assign_to=None):
        pass

    def _get_symbol(self, s):
        """Print the symbol appropriately."""
        return self.code_generator(s).strip()

    def _call_printer(self, routine):
        code_lines = []

        # Compose a list of symbols to be dereferenced in the function
        # body. These are the arguments that were passed by a reference
        # pointer, excluding arrays.
        dereference = []
        for arg in routine.arguments:
            if isinstance(arg, ResultBase) and not arg.dimensions:
                dereference.append(arg.name)

        for instruction in routine.instructions:
            constants, not_supported, expr = self.code_generator(instruction, human=False)
            code_lines.append("%s\n" % (expr))
        return code_lines

        # declarations = []
        # code_lines = []
        # for i, result in enumerate(routine.results):
        #     print(self.settings)
        #     constants, not_supported, jl_expr = self.code_generator(result, human=False, **self.settings)

        #     for obj, v in sorted(constants, key=str):
        #         declarations.append(
        #             "%s = %s\n" % (obj, v))
        #     for obj in sorted(not_supported, key=str):
        #         if isinstance(obj, Function):
        #             name = obj.func
        #         else:
        #             name = obj
        #         declarations.append(
        #             "# unsupported: %s\n" % (name))
        #     code_lines.append("%s\n" % (jl_expr))
        # return declarations + code_lines

    def _get_routine_opening(self, routine):
        return []

    def _get_routine_ending(self, routine):
        return []

class CythonCodeGen(LBMCodeGen):
    """Generator for Cython code.

    The .write() method inherited from CodeGen will output a code file <prefix>.pyx.

    """

    code_extension = "pyx"

    def code_generator(self, expr, assign_to=None, **settings):
        return cython_code(expr, assign_to, **settings)

    def _get_header(self):
        code_lines = ["#!python\n",
                      "#cython: boundscheck=False\n",
                      "#cython: wraparound=False\n",
                      "#cython: cdivision=True\n",
                      "#cython: binding=True\n",
                      "#import cython\n",
                      "from libc.math cimport *\n",
                      "import numpy as np\n",
                     ]
        return code_lines + ["\n\n"]

    def _preprocessor_statements(self, prefix):
        # code_lines = ["#!python\n",
        #               "#cython: boundscheck=False\n",
        #               "#cython: wraparound=False\n",
        #               "#cython: cdivision=True\n",
        #               "#cython: binding=True\n",
        #               "#import cython\n",
        #               "from libc.math cimport *\n",
        #              ]
        # return code_lines + ["\n\n"]
        return []

    def _get_routine_opening(self, routine):
        """Returns the opening statements of the routine."""
        code_list = []
        export = True
        # export = self.settings.pop('export', True)
        if export:
            code_list.append("def ")
        else:
            code_list.append("cdef void ")

        # Inputs
        args = []
        for i, arg in enumerate(routine.arguments):
            if isinstance(arg, OutputArgument):
                raise CodeGenError("Cython: invalid argument of type %s" %
                                   str(type(arg)))

            if isinstance(arg, (InputArgument, InOutArgument)):
                name = self._get_symbol(arg.name)
                if not arg.dimensions:
                    # If it is a scalar
                    if isinstance(arg, ResultBase):
                        # if it is an output
                        args.append((arg.get_datatype('C'), "*%s" % name))
                    else:
                        # if it is an output
                        args.append((arg.get_datatype('C'), name))
                else:
                    if not export and len(arg.dimensions) == 1:
                        # if the dimension is 1
                        args.append((arg.get_datatype('C'), "*%s" % name))
                    else:
                        args.append((arg.get_datatype('C') + '[' + ', '.join([':']*len(arg.dimensions)) + ':1]', "%s" % name))

        args = ", ".join([ "%s %s" % t for t in args])
        code_list.append("%s(%s)%s\n" % (routine.name, args, ":" if export else " nogil:"))
        code_list = [ "".join(code_list) ]

        return code_list

    def _declare_arguments(self, routine):
        return []

    def _declare_globals(self, routine):
        args = []
        for g in routine.local_vars:
            if isinstance(g, Symbol):
                args.append("cdef double %s\n"%(self._get_symbol(g)))
            else:
                shape = [d for d in g.shape if d!=1]
                if isinstance(g, Indexed):
                    args.append("cdef double[%s] %s = np.zeros((%s))\n"%(', '.join([':']*len(shape)) + ':1', g.base, ','.join("%s"%s for s in shape)))
                else:
                    args.append("cdef double %s[%s]\n"%(self._get_symbol(g), ','.join("%s"%s for s in shape)))
        return ["".join(args)]

    def _declare_locals(self, routine):
        s = []
        for l in routine.idx_vars:
            s.append("cdef int %s\n" % l.label)
        return s + ['\n']

    def _get_routine_ending(self, routine):
        return ["#end\n"]

    def _indent_code(self, codelines):
        p = CythonCodePrinter()
        return p.indent_code(codelines)

    def dump_pyx(self, routines, f, prefix, header=True, empty=True):
        self.dump_code(routines, f, prefix, header, empty)

    dump_pyx.extension = code_extension
    dump_pyx.__doc__ = CodeGen.dump_code.__doc__

    # This list of dump functions is used by CodeGen.write to know which dump
    # functions it has to call.
    dump_fns = [dump_pyx]

class NumpyCodeGen(LBMCodeGen):
    """Generator for Cython code.

    The .write() method inherited from CodeGen will output a code file <prefix>.pyx.

    """

    code_extension = "py"

    def code_generator(self, expr, assign_to=None, **settings):
        return numpy_code(expr, assign_to, **settings)

    def _get_header(self):
        code_lines = ["import numpy as np\n",
                     ]
        return code_lines + ["\n\n"]

    def _preprocessor_statements(self, prefix):
        return []

    def _get_routine_opening(self, routine):
        """Returns the opening statements of the routine."""
        code_list = []
        code_list.append("def ")

        # Inputs
        args = []
        for i, arg in enumerate(routine.arguments):
            if isinstance(arg, OutputArgument):
                raise CodeGenError("Numpy: invalid argument of type %s" %
                                   str(type(arg)))

            if isinstance(arg, (InputArgument, InOutArgument)):
                name = self._get_symbol(arg.name)
                args.append(name)
        args = ", ".join(args)
        code_list.append("%s(%s):\n" % (routine.name, args))
        code_list = [ "".join(code_list) ]

        return code_list

    def _declare_arguments(self, routine):
        return []

    def _declare_globals(self, routine):
        return []

    def _declare_locals(self, routine):
        return []

    def _get_routine_ending(self, routine):
        return ["#end\n"]

    def _indent_code(self, codelines):
        p = NumpyCodePrinter()
        return p.indent_code(codelines)

    def dump_py(self, routines, f, prefix, header=True, empty=True):
        self.dump_code(routines, f, prefix, header, empty)

    dump_py.extension = code_extension
    dump_py.__doc__ = CodeGen.dump_code.__doc__

    # This list of dump functions is used by CodeGen.write to know which dump
    # functions it has to call.
    dump_fns = [dump_py]

class LoopyCodeGen(LBMCodeGen):
    """Generator for Cython code.

    The .write() method inherited from CodeGen will output a code file <prefix>.pyx.

    """

    code_extension = "py"

    _default_settings = {"prefetch": None}

    def code_generator(self, expr, assign_to=None, **settings):
        return loopy_code(expr, assign_to, **settings)

    def _preprocessor_statements(self, prefix):
        return []

    def _get_header(self):
        code_lines = ["import loopy as lp\n",
                      "import numpy as np\n"
                     ]
        return code_lines + ["\n\n"]

    def _get_routine_opening(self, routine):
        """Returns the opening statements of the routine."""
        code_list = []
        code_list.append("%s = lp.make_kernel("%routine.name)
        name = []
        bounds = []
        for i in routine.idx_vars:
            if isinstance(i, Idx):
                name.append("%s_"%i.label)
                bounds.append("0<={ilabel}_<{upper}".format(ilabel=i.label, upper=i.upper-i.lower))

        if len(name) > 0:
            code_list.append('"{[%s]:%s}",'%(",".join(name), " and ".join(bounds)))
        code_list.append('"""  # noqa (silences flake8 line length warning)\n')
        code_list = [ "\n".join(code_list) ]
        return code_list

    def _declare_arguments(self, routine):
        return []

    def _declare_globals(self, routine):
        return []

    def _declare_locals(self, routine):
        return []

    def _get_routine_ending(self, routine):
        code_list = []
        code_list.append('""",')

        # Inputs
        args = []
        dtypes = []
        for i, arg in enumerate(routine.arguments):
            if isinstance(arg, OutputArgument):
                raise CodeGenError("Loopy: invalid argument of type %s" %
                                   str(type(arg)))

            if isinstance(arg, (InputArgument, InOutArgument)):
                name = self._get_symbol(arg.name)
                if arg.dimensions:
                    dims = ["{}".format(d[1]-d[0]+1) for d in arg.dimensions]
                    dtype = arg.get_datatype('PYTHON')
                    if dtype == 'int':
                        dtype = 'np.int32'
                    args.append('lp.GlobalArg("{name}", dtype={dtype}, shape="{shape}")'.format(name=name, dtype=dtype, shape=", ".join(dims)))
                else:
                    args.append('lp.ValueArg("{name}", dtype={dtype})'.format(name=name, dtype=arg.get_datatype('PYTHON')))
        for i, arg in enumerate(routine.local_vars):
            if isinstance(arg, Symbol):
                args.append('lp.TemporaryVariable("{name}", dtype=float)'.format(name=self._get_symbol(arg)))
            else:
                dims = [d for d in arg.shape if d!=1]
                args.append('lp.TemporaryVariable("{name}", dtype=float, shape="{shape}")'.format(name=self._get_symbol(arg), shape=','.join("%s"%s for s in dims)))

        code_list.append('[')
        args = ",\n".join(args)
        code_list.append(args)
        code_list.append('])#endArg\n')

        # add type
        dim = len(routine.idx_vars)
        if dim == 1:
            block_size = [256]
        if dim == 2:
            block_size = [16, 16]
        if dim == 3:
            block_size = [4, 4, 4]

        for i, idx in enumerate(routine.idx_vars[-1::-1]):
            code_list.append('{name} = lp.split_iname({name}, "{label}", {block}, outer_tag="g.{ilabel}", inner_tag="l.{ilabel}")'.format(name = routine.name,
            label = "%s_"%idx.label, ilabel=i, block=block_size[i]))
        code_list.append('{name} = lp.expand_subst({name})\n'.format(name=routine.name))
        code_list.append('{name} = lp.set_options({name}, no_numpy = True)\n'.format(name=routine.name))

        prefetch = routine.settings.get("prefetch", None)

        if prefetch:
            for var in prefetch:
                indices = []
                for idx in routine.idx_vars:
                    indices.append("%s__inner"%idx.label)
                # for i in range(var.rank):
                #     if isinstance(var.indices[i], Idx):
                #         indices.append("%s__inner"%var.indices[i].label)
                code_list.append('{name} = lp.add_prefetch({name}, "{var}", "{label}", fetch_bounding_box=True)\n'.format(name=routine.name, var=var.base.label, label=",".join(indices)))
#            print("PREFETCH")
        #     label = []
        #     for var in self._settings["prefetch"]:
        #         indices = []
        #         for i in var.indices:
        #             if isinstance(i, Idx):
        #                 indices.append("%s__inner"%i.label)
        #         code_list.append('{name} = lp.add_prefetch({name}, "{var}", "{label}", fetch_bounding_box=True)'.format(name=routine.name, var=var.label, label=",".join(indices)))
        #print(LoopyCodePrinter()._sort_optimized(routine.local_vars, routine.instructions))
# one_time_step = lp.split_iname(one_time_step, "ii", 16, outer_tag="g.1", inner_tag="l.1")
# one_time_step = lp.split_iname(one_time_step, "jj", 16, outer_tag="g.0", inner_tag="l.0")
# one_time_step = lp.expand_subst(one_time_step)
# one_time_step = lp.add_prefetch(one_time_step, "f", "ii_inner,jj_inner", fetch_bounding_box=True)
        code_list = [ "\n".join(code_list) ]
        return code_list

    def _indent_code(self, codelines):
        p = LoopyCodePrinter()
        return p.indent_code(codelines)

    def dump_py(self, routines, f, prefix, header=True, empty=True):
        self.dump_code(routines, f, prefix, header, empty)

    dump_py.extension = code_extension
    dump_py.__doc__ = CodeGen.dump_code.__doc__

    # This list of dump functions is used by CodeGen.write to know which dump
    # functions it has to call.
    dump_fns = [dump_py]

def get_code_generator(language, project):
    CodeGenClass = {"NUMPY" : NumpyCodeGen,
                    "CYTHON": CythonCodeGen,
                    "LOOPY": LoopyCodeGen}.get(language.upper())
    if CodeGenClass is None:
        raise ValueError("Language '%s' is not supported." % language)
    return CodeGenClass(project)

def codegen(name_expr, language, prefix=None, project="project",
            to_files=False, header=True, empty=True, argument_sequence=None,
            global_vars=None, settings={}):
    # Initialize the code generator.
    code_gen = get_code_generator(language, project)

    if isinstance(name_expr[0], string_types):
        # single tuple is given, turn it into a singleton list with a tuple.
        name_expr = [name_expr]

    if prefix is None:
        prefix = name_expr[0][0]

    # Construct Routines appropriate for this code_gen from (name, expr) pairs.
    routines = []
    for name, expr in name_expr:
        routines.append(code_gen.routine(name, expr, argument_sequence,
                                         global_vars, settings))

    # Write the code.
    return code_gen.write(routines, prefix, to_files, header, empty)

def make_routine(name_expr, argument_sequence=None, local_vars=None, settings={}):
    if isinstance(name_expr[0], string_types):
        # single tuple is given, turn it into a singleton list with a tuple.
        name_expr = [name_expr]

    routines = []
    for name, expr in name_expr:
        routines.append(LBMCodeGen().routine(name, expr, argument_sequence,
                                             local_vars, settings))

    return routines


#!/usr/bin/env python2.7

'''A tool for determining the UVIT FUV and NUV filters.

   Copyright 2018 Prajwel Joseph

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

   Changes; when, what
   -------------------
   Jan 10, 2018: Galactic latitude check incorporated.

   The author would like to acknowledge inputs from Dr. Koshy George
   which greatly helped the developement of this script.

'''

import os
import sys
import string
import urllib
import matplotlib
# Force matplotlib to not use any Xwindows backend.
matplotlib.use('Agg')
import numpy as np
import matplotlib.pyplot as plt

from astropy.io import fits
from astropy.wcs import WCS
from astropy.io import ascii
from requests import Session
from bs4 import BeautifulSoup
from astropy import units as u
from matplotlib.colors import LogNorm
from astropy.table import Table, hstack
from astropy.coordinates import SkyCoord

# To get the user input.
instrument = str(sys.argv[1])
RA = str(sys.argv[2])
DEC = str(sys.argv[3])
#working_arena = str(sys.argv[4])

#instrument = 'uvit'
#RA = "7:36:51.396"
#DEC = "65:36:9.170"
working_arena = '.'

# To do all the stuff in a specific directory.
os.chdir(working_arena)

# To read the TD1 catalogue.
try:
    catalogue = 'td1_catalogue.fits'
    hdu = fits.open(catalogue)
except IOError:
    print('Could not find the catalogue file: {}'.format(catalogue))
    sys.exit(1)

# instrument and radius of search in arsec.
field_radius = {'uvit'  : 1200,
                'sxt'   : 1500,
                'czti'  : 1680,
                'laxpc' : 1680}

# Functions to convert magnitude to UVIT count rates.
flux_norm = 2E-13
def countnuv(flux):
    silica = 955.0
    b4 = 218.5
    b13 = 275.8
    b15 = 59.6
    n2 = 50.6
    flux_ratio = flux / flux_norm
    cr1 =  silica * flux_ratio
    cr2 =  b4 * flux_ratio
    cr3 =  b13 * flux_ratio
    cr4 =  b15 * flux_ratio
    cr5 =  n2 * flux_ratio
    return flux, cr1, cr2, cr3, cr4, cr5

def countfuv(flux):
    caf2 = 74.5
    baf2 = 60.0
    sapphire = 50.0
    silica = 17.3
    flux_ratio = flux / flux_norm
    cr1 =  caf2 * flux_ratio
    cr2 =  baf2 * flux_ratio
    cr3 =  sapphire * flux_ratio
    cr4 =  silica * flux_ratio
    return flux, cr1, cr2, cr3, cr4

# Function to find seperation in celestial coordinates.
cc = SkyCoord(RA, DEC, unit = (u.hourangle, u.deg))
def cel_separation(a, b):
    coo = SkyCoord(a, b, frame = 'icrs', unit = 'deg')
    return coo.separation(cc)

# To check if Galactic latitude is between -30 to 30.
gal_lat = cc.galactic.b.value
gal_plane = 'no'
if -30.0 <= gal_lat <= 30.0:
    gal_plane_warning = 'The galactic latitude is between -30 to 30. \
                        \nYour field cannot be checked using TD1 catalogue!'
    print('\n{}\n'.format(gal_plane_warning))
    with open('gal_plane_warning.txt', 'w') as gal_warn:
        gal_warn.write(gal_plane_warning)
    sys.exit(1)

# Reading coordinates and fluxes from catalogue.
hdu = fits.open(catalogue)
alpha = hdu[1].data['ra']
delta = hdu[1].data['dec']
nuv_flux = hdu[1].data['flux_2365_a']
fuv_flux = hdu[1].data['flux_1565_a']

# NUV
refined_set = [(al, de, nf) for al, de, nf
                            in zip(alpha, delta, nuv_flux)
                            if (cc.ra.value - 5) <= al <= (cc.ra.value + 5)
                                and (cc.dec.value - 5) <= de <= (cc.dec.value + 5)]

nalpha, ndelta, nuv_flux = zip(*refined_set)

confined_set = [nf for al, de, nf
                   in zip(nalpha, ndelta, nuv_flux)
                   if cel_separation(al, de) <= field_radius[instrument] * u.arcsec]

# If list is empty, normal value need to be taken.
if len(confined_set) == 0:
    confined_set.append(flux_norm)

nd = sorted(confined_set)[-1]
flux, ta, tb, tc, td, te = countnuv(nd)
nuv_res = Table([[flux], [ta], [tb], [tc], [td], [te]],
               names = ('flux_2365_a',
                        'silica',
                        'b4',
                        'b13',
                        'b15',
                        'n2'),
               meta = {'name': 'NUV counts'})

#nuv_res['flux_2365_a'].format = '
nuv_res['silica'].format = '4.1f'
nuv_res['b4'].format = '4.1f'
nuv_res['b13'].format = '4.1f'
nuv_res['b15'].format = '4.1f'
nuv_res['n2'].format = '4.1f'

print('\n\n### NUV\n\n{}\n'.format(nuv_res))

# To select NUV safe filters.
nuv_filter_dict = {0: 'Silica', 1: 'NUV-B4', 2: 'NUV-B13', 3: 'NUV-B15', 4: 'NUV-N2'}
i = 0
nuv_safe = []
for Filter in zip(*nuv_res['silica','b4','b13','b15','n2']):
     if sum(np.array(Filter) > 1500) == 0:
         nuv_safe.append(nuv_filter_dict[i])
     if i == 0:
         if sum(np.array(Filter) > 1133) == 0:
             nuv_safe.append('NUV-grating')
     i = i + 1

nuv_declaration = 'Safe filters in NUV: {}'.format(nuv_safe)
print('\n\n{}\n'.format(nuv_declaration))

# To write to file.
nuv_table = 'NUV_td1-nd-int.txt'
ascii.write(nuv_res, nuv_table, format = 'csv', overwrite = True)

with open('safe_NUV_filters.txt', 'w') as safe_file:
    safe_file.write(nuv_declaration)

# FUV
refined_set = [(al, de, ff) for al, de, ff
                            in zip(alpha, delta, fuv_flux)
                            if (cc.ra.value - 5) <= al <= (cc.ra.value + 5)
                                and (cc.dec.value - 5) <= de <= (cc.dec.value + 5)]

nalpha, ndelta, fuv_flux = zip(*refined_set)

confined_set = [ff for al, de, ff
                   in zip(nalpha, ndelta, fuv_flux)
                   if cel_separation(al, de) <= field_radius[instrument] * u.arcsec]

# If list is empty, normal value need to be taken.
if len(confined_set) == 0:
    confined_set.append(flux_norm)

fd = sorted(confined_set)[-1]
flux, ta, tb, tc, td = countfuv(fd)
fuv_res = Table([[flux], [ta], [tb], [tc], [td]],
               names = ('flux_1565_a',
                        'caf2',
                        'baf2',
                        'sapphire',
                        'silica'),
               meta = {'name': 'NUV counts'})

#fuv_res['flux_1565_a'].format = '
fuv_res['caf2'].format = '4.1f'
fuv_res['baf2'].format = '4.1f'
fuv_res['sapphire'].format = '4.1f'
fuv_res['silica'].format = '4.1f'

print('\n### FUV \n\n{}\n\n'.format(fuv_res))

# To select FUV safe filters.
fuv_filter_dict = {0: 'CaF2', 1: 'BaF2', 2: 'Sapphire', 3: 'Silica'}
j = 0
fuv_safe = []
for Filter in zip(*fuv_res['caf2','baf2','sapphire','silica']):
     if sum(np.array(Filter) > 1500) == 0:
         fuv_safe.append(fuv_filter_dict[j])
     if j == 0:
         if sum(np.array(Filter) > 892) == 0:
             fuv_safe.append('FUV-grating')
     j = j + 1

fuv_declaration = 'Safe filters in FUV: {}'.format(fuv_safe)
print('\n\n{}\n'.format(fuv_declaration))

# To write to file.
fuv_table = 'FUV_td1-fd-int.txt'
ascii.write(fuv_res, fuv_table, format = 'csv', overwrite = True)

with open('safe_FUV_filters.txt', 'w') as safe_file:
    safe_file.write(fuv_declaration)

print('Done!\n')

Gaia (UV filter checking tool)/td1_gaia_V.2.5.py