#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Apr  5 14:42:49 2021

@author: omartin
"""

# IMPORTING PYTHON LIBRAIRIES
import os.path as ospath
import pathlib
from configparser import ConfigParser
import numpy as np

import yaml

# IMPORTING P3 MODULES
from p3.aoSystem.telescope import telescope
from p3.aoSystem.atmosphere import atmosphere
from p3.aoSystem.source import source
from p3.aoSystem.deformableMirror import deformableMirror
from p3.aoSystem.detector import detector
from p3.aoSystem.sensor import sensor
from p3.aoSystem.rtc import rtc
import p3.aoSystem.anisoplanatismModel as anisoplanatismModel

#%%
class aoSystem():

    def raiseMissingRequiredOpt(self,sec,opt):
        raise ValueError("'{}' is missing from section '{}'"
                         .format(opt,sec))
        
    def raiseMissingRequiredSec(self,sec):
        raise ValueError("The section '{}' is missing from the parameter file"
                         .format(sec))
        
    def raiseNotSameLength(self,sec,opt):
        raise ValueError("'{}' in section '{}' must have the same length"
                         .format(*opt,sec))

    def check_section_key(self, primary):        
        return primary in self.my_data_map.keys()

    def check_config_key(self, primary, secondary):
        if primary in self.my_data_map.keys():
            return secondary in self.my_data_map[primary].keys()
        else:
            return False

    def get_config_value(self, primary, secondary):
        return self.my_data_map[primary][secondary]
    
    def resolve_path(self, path_value):
        """Use enhanced path resolution with TIPTOP support"""
        from p3.aoSystem import resolve_config_path, PATH_TIPTOP, detect_tiptop_path
        
        path_p3 = str(pathlib.Path(__file__).parent.parent.absolute())
        # lazily re-try TIPTOP detection if it was None at import time
        path_tiptop = PATH_TIPTOP or detect_tiptop_path()

        return resolve_config_path(path_value, self.path_root, path_p3, path_tiptop)

    def __init__(self,path_config,path_root='',getPSDatNGSpositions=False,
                psdExpansion=False,coo_stars=None):

        if path_root is None:
            path_root = ''
        self.path_root = path_root

        self.coo_stars = coo_stars
        self.psdExpansion = psdExpansion
        self.error = False
        # verify if the file exists
        if ospath.isfile(path_config) == False:
            raise ValueError('The parameter file (.ini or .yml) could not be found.' + str(path_config))

        if path_config[-4::]=='.ini':
            # open the .ini file
            config = ConfigParser()
            config.optionxform = str
            config.read(path_config)
            self.my_data_map = {} 
            for section in config.sections():
                self.my_data_map[section] = {}
                for name,value in config.items(section):
                    self.my_data_map[section].update({name:eval(value)})

        elif path_config[-4::]=='.yml':
            with open(path_config) as f:
                my_yaml_dict = yaml.safe_load(f)
            self.my_data_map = my_yaml_dict

        self.getPSDatNGSpositions = getPSDatNGSpositions

        #%% TELESCOPE
        #----- grabbing main parameters
        if not(self.check_section_key('telescope')):
            self.raiseMissingRequiredSec('telescope')
        
        if self.check_config_key('telescope','TelescopeDiameter'):
            self.D = self.get_config_value('telescope','TelescopeDiameter')
        else:
            self.raiseMissingRequiredOpt('telescope','TelescopeDiameter')

        if self.check_config_key('telescope','ZenithAngle'):
            zenithAngle = self.get_config_value('telescope','ZenithAngle')
            if zenithAngle >= 90:
                raise ValueError("'zenithAngle' [deg] cannot be greater than or equal to 90.")
        else:
            zenithAngle = 0.0

        airmass = 1/np.cos(zenithAngle*np.pi/180)

        if self.check_config_key('telescope','ObscurationRatio'):
            obsRatio = self.get_config_value('telescope','ObscurationRatio')
        else:
            obsRatio = 0.0

        if self.check_config_key('telescope','Resolution'):
            nPup = self.get_config_value('telescope','Resolution')
        else:
            nPup = 256

        #----- PUPIL
        if self.check_config_key('telescope','PathPupil'):
            PathPupil = self.get_config_value('telescope','PathPupil')
            path_pupil = self.resolve_path(PathPupil)          
            if path_pupil and not ospath.isfile(path_pupil):
                raise ValueError(f"PathPupil file not found: {path_pupil}")
        else:
            path_pupil = ''

        if self.check_config_key('telescope','PupilAngle'):
            pupilAngle = self.get_config_value('telescope','PupilAngle')
        else:
            pupilAngle = 0.0

        if self.check_config_key('telescope','PathStaticOn'):
            PathStaticOn = self.get_config_value('telescope','PathStaticOn')
            path_static_on = self.resolve_path(PathStaticOn)
            if path_static_on and not ospath.isfile(path_static_on):
                raise ValueError(f"PathStaticOn file not found: {path_static_on}")
        else:
            path_static_on = None

        if self.check_config_key('telescope','zCoefStaticOn'):
            zCoefStaticOn = self.get_config_value('telescope','zCoefStaticOn')
        else:
            zCoefStaticOn = []

        if self.check_config_key('telescope','PathStaticOff'):
            PathStaticOff = self.get_config_value('telescope','PathStaticOff')
            path_static_off = self.resolve_path(PathStaticOff)
            if path_static_off and not ospath.isfile(path_static_off):
                raise ValueError(f"PathStaticOff file not found: {path_static_off}")
        else:
            path_static_off = None

        if self.check_config_key('telescope','PathStaticPos'):
            PathStaticPos = self.get_config_value('telescope','PathStaticPos')
            path_static_pos = self.resolve_path(PathStaticPos)
            if path_static_pos and not ospath.isfile(path_static_pos):
                raise ValueError(f"PathStaticPos file not found: {path_static_pos}")    
        else:
            path_static_pos = None

        #----- APODIZER
        if self.check_config_key('telescope','PathApodizer'):
            PathApodizer = self.get_config_value('telescope','PathApodizer')
            path_apodizer = self.resolve_path(PathApodizer)
            if path_apodizer and not ospath.isfile(path_apodizer):
                raise ValueError(f"PathApodizer file not found: {path_apodizer}")
        else:
            path_apodizer = ''
  
        #----- TELESCOPE ABERRATIONS
        if self.check_config_key('telescope', 'PathStatModes'):
            PathStatModes = self.get_config_value('telescope','PathStatModes')
            path_statModes = self.resolve_path(PathStatModes)
            if path_statModes and not ospath.isfile(path_statModes):
                raise ValueError(f"PathStatModes file not found: {path_statModes}")
        else:
            path_statModes = ''

        #----- EXTRA ERROR
        if self.check_config_key('telescope', 'extraErrorNm'):       
            extraErrorNm = self.get_config_value('telescope','extraErrorNm')
        else:
            extraErrorNm = 0

        if self.check_config_key('telescope', 'extraErrorExp'):
            extraErrorExp = self.get_config_value('telescope','extraErrorExp')
        else:
            extraErrorExp = -2

        if self.check_config_key('telescope', 'extraErrorMin'):
            extraErrorMin = self.get_config_value('telescope','extraErrorMin')
        else:
            extraErrorMin = 0

        if self.check_config_key('telescope', 'extraErrorMax'):
            extraErrorMax = self.get_config_value('telescope','extraErrorMax')
        else:
            extraErrorMax = 0

        if self.check_config_key('telescope', 'extraErrorLoNm'): 
            extraErrorLoNm = self.get_config_value('telescope','extraErrorLoNm')
        else:
            extraErrorLoNm = extraErrorNm 

        if self.check_config_key('telescope', 'extraErrorLoExp'):
            extraErrorLoExp = self.get_config_value('telescope','extraErrorLoExp')
        else:
            extraErrorLoExp = extraErrorExp

        if self.check_config_key('telescope', 'extraErrorLoMin'):
            extraErrorLoMin = self.get_config_value('telescope','extraErrorLoMin')
        else:
            extraErrorLoMin = 0

        if self.check_config_key('telescope', 'extraErrorLoMax'):
            extraErrorLoMax = self.get_config_value('telescope','extraErrorLoMax')
        else:
            extraErrorLoMax = 0

        # ----- class definition     
        self.tel = telescope(self.D, nPup,
                             zenith_angle=zenithAngle,
                             obsRatio=obsRatio,
                             pupilAngle=pupilAngle,
                             path_pupil=path_pupil,
                             path_static_on=path_static_on,
                             zcoef_static_on=zCoefStaticOn,
                             path_static_off=path_static_off,
                             path_static_pos=path_static_pos,
                             path_apodizer=path_apodizer,
                             path_statModes=path_statModes,
                             extraErrorNm=extraErrorNm,
                             extraErrorExp=extraErrorExp,
                             extraErrorMin=extraErrorMin,
                             extraErrorMax=extraErrorMax,
                             extraErrorLoNm=extraErrorLoNm,
                             extraErrorLoExp=extraErrorLoExp,
                             extraErrorLoMin=extraErrorLoMin,
                             extraErrorLoMax=extraErrorLoMax)

        if self.check_config_key('telescope', 'TechnicalFoV'):
            self.TechnicalFoV = self.get_config_value('telescope','TechnicalFoV')
        else:
            self.TechnicalFoV = 0

        if self.check_config_key('telescope', 'windPsdFile'):
            windPsdFile_val = self.get_config_value('telescope','windPsdFile')
            self.windPsdFile = self.resolve_path(windPsdFile_val)
            if self.windPsdFile and not ospath.isfile(self.windPsdFile):
                raise ValueError(f"windPsdFile not found: {self.windPsdFile}")
        else:
            self.windPsdFile = 0

        #%% ATMOSPHERE
        if not(self.check_section_key('atmosphere')):
            self.raiseMissingRequiredSec('atmosphere')

        if self.check_config_key('atmosphere','Wavelength'):
            wvlAtm = self.get_config_value('atmosphere','Wavelength')
        else:
            wvlAtm = 500e-9

        if self.check_config_key('atmosphere','Seeing'):
            r0 = 0.976*wvlAtm/self.get_config_value('atmosphere','Seeing')*3600*180/np.pi
        else:
            if self.check_config_key('atmosphere','r0_value'):
                r0 = self.get_config_value('atmosphere','r0_value')
            else:
                self.raiseMissingRequiredOpt('atmosphere','Seeing')

        if self.check_config_key('atmosphere','L0'):
            L0 = self.get_config_value('atmosphere','L0')
        else:
            L0 = 25

        if self.check_config_key('atmosphere','Cn2Weights'):
            weights = self.get_config_value('atmosphere','Cn2Weights')
            if np.abs(np.sum(weights) - 1) > 1e-3:
                raise ValueError("Sum of 'Cn2Weights' in section 'atmosphere' is not equal to 1")
        else:
            weights = [1.0]

        if self.check_config_key('atmosphere','Cn2Heights'):
            heights = self.get_config_value('atmosphere','Cn2Heights')
        else:
            heights = [0.0]

        if self.check_config_key('atmosphere','WindSpeed'):
            wSpeed = self.get_config_value('atmosphere','WindSpeed')
        else:
            wSpeed = [10.0]

        if self.check_config_key('atmosphere','WindDirection'):
            wDir = self.get_config_value('atmosphere','WindDirection')
        else:
            wDir = list(np.array(wSpeed)*0.)

        #-----  verification
        if not (len(weights) == len(heights) == len(wSpeed) == len(wDir)):
            self.raiseNotSameLength('atmosphere',['Cn2Weights','Cn2Heights','WindSpeed','WindDirection'])

        #----- class definition
        self.atm = atmosphere(wvlAtm, r0*airmass**(-3.0/5.0),
                              weights,
                              np.array(heights)*airmass,
                              wSpeed,
                              wDir,
                              L0)            
        
        #%%  GUIDE STARS
        if not(self.check_section_key('sources_HO')):
            self.raiseMissingRequiredSec('sources_HO')

        if self.check_config_key('sources_HO','Wavelength'):
            self.wvlGs     = np.atleast_1d(self.get_config_value('sources_HO','Wavelength'))
        else:
            self.raiseMissingRequiredOpt('sources_HO', 'Wavelength')
            return 0

        if self.check_config_key('sources_HO','Zenith'):
            self.zenithGs = self.get_config_value('sources_HO','Zenith')
        else:
            self.zenithGs = [0.0]

        if self.check_config_key('sources_HO','Azimuth'):
            self.azimuthGs = self.get_config_value('sources_HO','Azimuth')
        else:
            self.azimuthGs = [0.0]

        if self.check_config_key('sources_HO','Height'):
            heightGs  = self.get_config_value('sources_HO','Height')
        else:
            heightGs = 0.0

        # ----- verification
        if len(self.zenithGs) != len(self.azimuthGs):
            self.raiseNotSameLength('sources_HO', ['Azimuth','Zenith'])

        self.ngs = None
        self.lgs = None
        # ----- creating the source class
        if heightGs == 0 and not self.check_section_key('sources_LO'):
            self.ngs = source(self.wvlGs,
                              self.zenithGs,self.azimuthGs,
                              tag="NGS",verbose=True)
        else:
            self.lgs = source(self.wvlGs,
                              self.zenithGs,self.azimuthGs,
                              height=heightGs*airmass,
                              tag="LGS",verbose=True)

        self.configLO()
        self.configLO_SC()

    def configLO_SC(self):
        #%%  SCIENCE SOURCES
        if not(self.check_section_key('sources_science')):
            self.raiseMissingRequiredSec('sources_science')

        if self.check_config_key('sources_science','Wavelength'):
            wvlSrc = np.atleast_1d(self.get_config_value('sources_science','Wavelength'))
        else:
            self.raiseMissingRequiredOpt('sources_science', 'Wavelength')

        if self.check_config_key('sources_science','Zenith'):
            zenithSrc = self.get_config_value('sources_science','Zenith')
        else:
            zenithSrc = [0.0]

        if self.check_config_key('sources_science','Azimuth'):
            azimuthSrc = self.get_config_value('sources_science','Azimuth')
        else:
            azimuthSrc = [0.0]

        if np.any(self.coo_stars):
            zenithSrc = np.hypot(self.coo_stars[0],self.coo_stars[1])
            azimuthSrc = np.arctan2(self.coo_stars[0],self.coo_stars[1])

        #----- verification
        if len(zenithSrc) != len(azimuthSrc):
            self.raiseNotSameLength('sources_science', ['Zenith','Azimuth'])
            return

        if self.getPSDatNGSpositions and self.check_config_key('sources_LO','Wavelength'):
            zenithSrc = zenithSrc +  (self.get_config_value('sources_LO','Zenith'))
            azimuthSrc = azimuthSrc + (self.get_config_value('sources_LO','Azimuth'))

        #----- class definition
        self.src = source(wvlSrc,
                          zenithSrc, azimuthSrc,
                          tag="SCIENCE",verbose=True)

        #%% HIGH-ORDER WAVEFRONT SENSOR
        if not(self.check_section_key('sensor_HO')):
            self.raiseMissingRequiredSec('sensor_HO')

        if self.check_config_key('sensor_HO','PixelScale'):
            psInMas = self.get_config_value('sensor_HO','PixelScale')
        else:
            self.raiseMissingRequiredOpt('sensor_HO', 'PixelScale')

        if self.check_config_key('sensor_HO','FieldOfView'):
            fov = self.get_config_value('sensor_HO','FieldOfView')
        else:
            self.raiseMissingRequiredOpt('sensor_HO', 'FieldOfView')

        if self.check_config_key('sensor_HO','Binning'):
            Binning = self.get_config_value('sensor_HO','Binning')
        else:
            Binning = 1

        if self.check_config_key('sensor_HO','SpotFWHM'):
            spotFWHM = self.get_config_value('sensor_HO','SpotFWHM')
        else:
            spotFWHM = [[0.0, 0.0]]

        if self.check_config_key('sensor_HO','NumberPhotons'):
            nphHO = self.get_config_value('sensor_HO','NumberPhotons')
        else:
            nphHO = [np.inf]

        if self.check_config_key('sensor_HO','SigmaRON'):
            ron = self.get_config_value('sensor_HO','SigmaRON')
        else:
            ron = 0.0

        if self.check_config_key('sensor_HO','Gain'):
            self.detectorGainHO = self.get_config_value('sensor_HO','Gain')
        else:
            self.detectorGainHO = 1

        if self.check_config_key('sensor_HO','SkyBackground'):
            sky = self.get_config_value('sensor_HO','SkyBackground')
        else:
            sky = 0.0

        if self.check_config_key('sensor_HO','Dark'):
            dark = self.get_config_value('sensor_HO','Dark')
        else:
            dark = 0.0

        if self.check_config_key('sensor_HO','ExcessNoiseFactor'):
            excess = self.get_config_value('sensor_HO','ExcessNoiseFactor')
        else:
            excess = 1.0

        if self.check_config_key('sensor_HO','SpectralBandwidth'):
            bw = self.get_config_value('sensor_HO','SpectralBandwidth')
        else:
            bw = 0.0
        if self.check_config_key('sensor_HO','Transmittance'):
            tr = self.get_config_value('sensor_HO','Transmittance')
        else:
            tr = [1.0]
        if self.check_config_key('sensor_HO','Dispersion'):
            disp = self.get_config_value('sensor_HO','Dispersion')
        else:
            disp = [[0.0], [0.0]]

        if self.check_config_key('sensor_HO','WfsType'):
            wfstype = self.get_config_value('sensor_HO','WfsType')
        else:
            wfstype = 'Shack-Hartmann'

        if self.check_config_key('sensor_HO','NumberLenslets'):
            nL = self.get_config_value('sensor_HO','NumberLenslets')
        else:
            nL = [20]

        if self.check_config_key('sensor_HO','SizeLenslets'):
            dsub = self.get_config_value('sensor_HO','SizeLenslets')
        else:
            dsub = list(self.D/np.array(nL))

        if self.check_config_key('sensor_HO','Modulation'):
            modu = self.get_config_value('sensor_HO','Modulation')
        else:
            modu = None

        if self.check_config_key('sensor_HO','NoiseVariance'):
            NoiseVar = np.array(self.get_config_value('sensor_HO','NoiseVariance'))
        else:
            NoiseVar = [None]

        if self.check_config_key('sensor_HO','Algorithm'):
            algorithm = self.get_config_value('sensor_HO','Algorithm')
        else:
            algorithm = 'wcog'

        if self.check_config_key('sensor_HO','WindowRadiusWCoG'):
            wr = self.get_config_value('sensor_HO','WindowRadiusWCoG')
        else:
            wr = 5.0

        if self.check_config_key('sensor_HO','ThresholdWCoG = 0.0'):
            thr = self.get_config_value('sensor_HO','ThresholdWCoG = 0.0')
        else:
            thr = 0.0

        if self.check_config_key('sensor_HO','NewValueThrPix'):
            nv = self.get_config_value('sensor_HO','NewValueThrPix')
        else:
            nv = 0.0

        if self.check_config_key('sensor_HO','ExcessNoiseFactor'):
            excess = self.get_config_value('sensor_HO','ExcessNoiseFactor')
        else:
            excess = 1.0

        if self.check_config_key('sensor_HO','addMcaoWFsensConeError'):
            self.addMcaoWFsensConeError = self.get_config_value('sensor_HO','addMcaoWFsensConeError')
        else:
            self.addMcaoWFsensConeError = False

        self.wfs = sensor(psInMas, fov,
                          binning=Binning, spotFWHM=spotFWHM,
                          nph=nphHO, bandwidth=bw, transmittance=tr, dispersion=disp,
                          gain=self.detectorGainHO, ron=ron, sky=sky, dark=dark, excess=excess,
                          nL=nL, dsub=dsub, wfstype=wfstype, modulation=modu,
                          noiseVar=NoiseVar, algorithm=algorithm,
                          algo_param=[wr,thr,nv], tag="HO WFS")
#%% TIP-TILT SENSORS
        if self.check_section_key('sensor_LO'):
            self.configLOsensor()
        else:
            self.tts = None

 #%% REAL-TIME-COMPUTER

        if self.check_config_key('RTC','LoopGain_HO'):
            self.LoopGain_HO = self.get_config_value('RTC','LoopGain_HO')
        else:
            self.LoopGain_HO = 0.5

        if self.check_config_key('RTC','SensorFrameRate_HO'):
            frameRate_HO = self.get_config_value('RTC','SensorFrameRate_HO')
        else:
            frameRate_HO = 500.0

        if self.check_config_key('RTC','LoopDelaySteps_HO'):
            delay_HO = self.get_config_value('RTC','LoopDelaySteps_HO')
        else:
            delay_HO = 2

        if self.check_config_key('RTC','LoopGain_LO'):
            temp = self.get_config_value('RTC','LoopGain_LO')
            if temp != 'optimize':
                self.LoopGain_LO = temp
            else:
                self.LoopGain_LO = None
        else:
            self.LoopGain_LO = None

        if self.check_config_key('RTC','SensorFrameRate_LO'):
            frameRate_LO = self.get_config_value('RTC','SensorFrameRate_LO')
        else:
            frameRate_LO = None

        if self.check_config_key('RTC','LoopDelaySteps_LO'):
            delay_LO = self.get_config_value('RTC','LoopDelaySteps_LO')
        else:
            delay_LO = None

        if self.check_config_key('RTC','ResidualError'):
            wfe = self.get_config_value('RTC','ResidualError')
        else:
            wfe = None

        self.rtc = rtc(self.LoopGain_HO, frameRate_HO, delay_HO, wfe=wfe,
                       loopGainLO=self.LoopGain_LO, frameRateLO=frameRate_LO, delayLO=delay_LO)

#%% DEFORMABLE MIRRORS
        if not(self.check_section_key('DM')):
            self.raiseMissingRequiredSec('DM')

        if self.check_config_key('DM','DmPitchs'):
            DmPitchs = np.array(self.get_config_value('DM','DmPitchs'))
        else:
            self.raiseMissingRequiredOpt('DM','DmPitchs')
            self.error = True
            return

        if self.check_config_key('DM','DmHeights'):
            DmHeights = self.get_config_value('DM','DmHeights')
        else:
            if len(DmPitchs) > 1:
                DmHeights = self.raiseMissingRequiredOpt('DM','DmHeights')
            else:
                DmHeights = [0.0]

        if self.check_config_key('DM','NumberActuators'):
            nActu = self.get_config_value('DM','NumberActuators')
        else:
            asec2rad = np.pi / (3600 * 180)
            DmSize = list(self.TechnicalFoV * asec2rad * np.array(DmHeights) + self.D)
            nActu = list((np.array(DmSize) / np.array(DmPitchs)).round().astype(int))

        if self.check_config_key('DM','InfModel'):
            InfModel = self.get_config_value('DM','InfModel')
        else:
            InfModel = 'gaussian'

        if self.check_config_key('DM','InfCoupling'):
            InfCoupling = self.get_config_value('DM','InfCoupling')
        else:
            InfCoupling = [0.2]

        if self.check_config_key('DM','OptimizationWeight'):
            opt_w = self.get_config_value('DM','OptimizationWeight')
        else:
            opt_w = [0.0]

        if self.check_config_key('DM','OptimizationAzimuth'):
            opt_az = self.get_config_value('DM','OptimizationAzimuth')
        else:
            opt_az = [0.0]

        if self.check_config_key('DM','OptimizationZenith'):
            opt_zen = self.get_config_value('DM','OptimizationZenith')
        else:
            opt_zen = [0.0]

        # ----- verification
        if (len(opt_zen) != len(opt_az)) or (len(opt_zen) != len(opt_w)):
            self.raiseNotSameLength('DM', ['OptimizationZenith','OptimizationAzimuth','OptimizationWeight'])

        if self.check_config_key('DM','OptimizationConditioning'):
            cond = self.get_config_value('DM','OptimizationConditioning')
        else:
            cond = 100.0

        if self.check_config_key('DM','NumberReconstructedLayers'):
            nrec = self.get_config_value('DM','NumberReconstructedLayers')
        else:
            nrec = 10

        if self.check_config_key('DM','AoArea'):
            AoArea = self.get_config_value('DM','AoArea')
        else:
            AoArea = 'circle'

        # ----- creating the dm class
        self.dms = deformableMirror(nActu, DmPitchs,
                                    heights=DmHeights, mechCoupling=InfCoupling,
                                    modes=InfModel,
                                    opt_dir=[opt_zen,opt_az],
                                    opt_weights=opt_w,
                                    opt_cond=cond,n_rec = nrec,
                                    AoArea=AoArea)

        #%% SCIENCE DETECTOR
        if not(self.check_section_key('sensor_science')):
            self.raiseMissingRequiredSec('sensor_science')

        if self.check_config_key('sensor_science','Name'):
            camName = self.get_config_value('sensor_science','Name')
        else:
            camName = 'SCIENCE CAM'

        if self.check_config_key('sensor_science','PixelScale'):
            psInMas = self.get_config_value('sensor_science','PixelScale')
        else:
            self.raiseMissingRequiredOpt('sensor_science', 'PixelScale')

        if self.check_config_key('sensor_science','FieldOfView'):
            fov = self.get_config_value('sensor_science','FieldOfView')
            if self.psdExpansion and len(wvlSrc) > 1:
                fov = int(fov * np.ceil(max(wvlSrc)/min(wvlSrc)))
        else:
            self.raiseMissingRequiredOpt('sensor_science', 'FieldOfView')

        if self.check_config_key('sensor_science','Binning'):
            Binning = self.get_config_value('sensor_science','Binning')
        else:
            Binning = 1

        if self.check_config_key('sensor_science','SpotFWHM'):
            spotFWHM = self.get_config_value('sensor_science','SpotFWHM')
        else:
            spotFWHM = [[0.0,0.0,0.0]]

        if self.check_config_key('sensor_science','SpectralBandwidth'):
            bw = self.get_config_value('sensor_science','SpectralBandwidth')
        else:
            bw = 0.0
        if self.check_config_key('sensor_science','Transmittance'):
            tr = self.get_config_value('sensor_science','Transmittance')
        else:
            tr = [1.0]
        if self.check_config_key('sensor_science','Dispersion'):
            disp = self.get_config_value('sensor_science','Dispersion')
        else:
            disp = [[0.0],[0.0]]

        if self.check_config_key('sensor_science','NumberPhotons'):
            nphSC = self.get_config_value('sensor_science','NumberPhotons')
        else:
            nphSC = np.inf

        if self.check_config_key('sensor_science','Saturation'):
            saturation = self.get_config_value('sensor_science','Saturation')
        else:
            saturation = np.inf

        if self.check_config_key('sensor_science','SigmaRON'):
            ron = self.get_config_value('sensor_science','SigmaRON')
        else:
            ron = 0.0

        if self.check_config_key('sensor_science','Gain'):
            self.detectorGainScience = self.get_config_value('sensor_science','Gain')
        else:
            self.detectorGainScience = 1

        if self.check_config_key('sensor_science','SkyBackground'):
            sky = self.get_config_value('sensor_science','SkyBackground')
        else:
            sky = 0.0

        if self.check_config_key('sensor_science','Dark'):
            dark = self.get_config_value('sensor_science','Dark')
        else:
            dark = 0.0

        if self.check_config_key('sensor_science','ExcessNoiseFactor'):
            excess = self.get_config_value('sensor_science','ExcessNoiseFactor')
        else:
            excess = 1.0

        self.cam = detector(psInMas, fov, binning=Binning, spotFWHM=spotFWHM, saturation=saturation,
                            nph=nphSC, bandwidth=bw, transmittance=tr, dispersion=disp,
                            gain=self.detectorGainScience, ron=ron, sky=sky, dark=dark, excess=excess,
                            tag=camName)

        # %% AO mode
        self.aoMode = 'SCAO'

        if self.lgs:
            if self.lgs.nSrc > 1:
                if self.dms.nDMs > 1:
                    self.aoMode = 'MCAO'
                else:
                    if self.dms.nRecLayers >1:
                        self.aoMode = 'LTAO'
                    else:
                        self.aoMode = 'GLAO'
            else:
                self.aoMode = 'SLAO'

        # %% ERROR BREAKDOWN
        if self.rtc.holoop['gain'] > 0:
            self.errorBreakdown()


    def configLO(self):

        if not self.check_section_key('sources_LO'):

            print('Warning: No information about the tip-tilt star can be retrieved')
        else:
            if self.check_config_key('sources_LO','Wavelength'):
                self.wvlGsLO = np.atleast_1d(self.get_config_value('sources_LO','Wavelength'))
            else:
                self.raiseMissingRequiredOpt('sources_LO','Wavelength')

            self.zenithGsLO   = self.get_config_value('sources_LO','Zenith')
            self.azimuthGsLO  = self.get_config_value('sources_LO','Azimuth')
            # ----- verification
            if len(self.zenithGsLO) != len(self.azimuthGsLO):
                self.raiseNotSameLength('sources_LO', ['Zenith','Azimuth'])

            self.ngs = source(self.wvlGsLO,self.zenithGsLO,self.azimuthGsLO,tag="NGS",verbose=True)


    def configLOsensor(self):

        if self.check_config_key('sensor_LO','PixelScale'):
            psInMas = self.get_config_value('sensor_LO','PixelScale')
        else:
            self.raiseMissingRequiredOpt('sensor_LO', 'PixelScale')
            self.error = True
            return

        if self.check_config_key('sensor_LO','FieldOfView'):
            fov = self.get_config_value('sensor_LO','FieldOfView')
        else:
            self.raiseMissingRequiredOpt('sensor_LO', 'FieldOfView')
            self.error = True
            return

        if self.check_config_key('sensor_LO','Binning'):
            Binning = self.get_config_value('sensor_LO','Binning')
        else:
            Binning = 1

        if self.check_config_key('sensor_LO','SpotFWHM'):
            spotFWHM = self.get_config_value('sensor_LO','SpotFWHM')
        else:
            spotFWHM = [[0.0, 0.0]]

        if self.check_config_key('sensor_LO','NumberPhotons'):
            nphLO = self.get_config_value('sensor_LO','NumberPhotons')
        else:
            nphLO = np.inf

        if self.check_config_key('sensor_LO','Gain'):
            self.detectorGainLO = self.get_config_value('sensor_LO','Gain')
        else:
            self.detectorGainLO = 1

        if self.check_config_key('sensor_LO','SigmaRON'):
            ron = self.get_config_value('sensor_LO','SigmaRON')
        else:
            ron = 0.0

        if self.check_config_key('sensor_LO','SkyBackground'):
            sky = self.get_config_value('sensor_LO','SkyBackground')
        else:
            sky = 0.0

        if self.check_config_key('sensor_LO','Dark'):
            dark = self.get_config_value('sensor_LO','Dark')
        else:
            dark = 0.0

        if self.check_config_key('sensor_LO','ExcessNoiseFactor'):
            excess = self.get_config_value('sensor_LO','ExcessNoiseFactor')
        else:
            excess = 1.0

        if self.check_config_key('sensor_LO','SpectralBandwidth'):
            bw = self.get_config_value('sensor_LO','SpectralBandwidth')
        else:
            bw = 0.0
        if self.check_config_key('sensor_LO','Transmittance'):
            tr = self.get_config_value('sensor_LO','Transmittance')
        else:
            tr = [1.0]
        if self.check_config_key('sensor_LO','Dispersion'):
            disp = self.get_config_value('sensor_LO','Dispersion')
        else:
            disp = [[0.0], [0.0]]

        if self.check_config_key('sensor_LO','NumberLenslets'):
            nL = self.get_config_value('sensor_LO','NumberLenslets')
        else:
            nL = [1]

        if self.check_config_key('sensor_LO','NoiseVariance'):
            NoiseVar = self.get_config_value('sensor_LO','NoiseVariance')
        else:
            NoiseVar = [None]

        if self.check_config_key('sensor_LO','Algorithm'):
            algorithm = self.get_config_value('sensor_LO','Algorithm')
        else:
            algorithm = 'wcog'

        if self.check_config_key('sensor_LO','WindowRadiusWCoG'):
            temp = self.get_config_value('sensor_LO','WindowRadiusWCoG')
            if temp != 'optimize':
                wr = temp
            else:
                wr = 1.0
        else:
            wr = 1.0

        if self.check_config_key('sensor_LO','ThresholdWCoG = 0.0'):
            thr = self.get_config_value('sensor_LO','ThresholdWCoG = 0.0')
        else:
            thr = 0.0

        if self.check_config_key('sensor_LO','NewValueThrPix'):
            nv = self.get_config_value('sensor_LO','NewValueThrPix')
        else:
            nv = 0.0

        self.tts = sensor(psInMas, fov,
                          binning=Binning, spotFWHM=spotFWHM,
                          nph=nphLO, bandwidth=bw, transmittance=tr, dispersion=disp,
                          gain=self.detectorGainLO, ron=ron, sky=sky, dark=dark, excess=excess,
                          nL=nL, dsub=list(self.D/np.array(nL)),
                          wfstype='Shack-Hartmann', noiseVar=NoiseVar,
                          algorithm=algorithm, algo_param=[wr,thr,nv], tag="TT WFS")


    def __repr__(self):

        s = '\t\t\t\t________________________'+ self.aoMode + ' SYSTEM ________________________\n\n'
        s += self.src.__repr__() + '\n'
        if self.lgs:
            s += self.lgs.__repr__() + '\n'
        if self.ngs:
            s += self.ngs.__repr__() + '\n'
        s += self.atm.__repr__() + '\n'
        s+= self.tel.__repr__() + '\n'
        s+= self.wfs.__repr__() +'\n'
        if self.tts:
            s+= self.tts.__repr__() +'\n'

        s+= self.rtc.__repr__() +'\n'
        s+= self.dms.__repr__() +'\n'
        s+= self.cam.__repr__()

        return s

    def errorBreakdown(self):
        """
            Computing the AO wavefront error breakdown based on theoretical formula
        """

        rad2nm = lambda x:  np.sqrt(x) * self.atm.wvl*1e9/2/np.pi
        self.wfe = dict()

        Dr053 = (self.tel.D/self.atm.r0)**(5/3)
        dactur053 = (self.dms.pitch[0]/self.atm.r0)**(5/3)
        dsubr053 = (self.wfs.optics[0].dsub/self.atm.r0)**(5/3)

        # DM fitting error
        self.wfe['DM fitting'] = rad2nm(0.23*dactur053)

        # Aliasing error
        self.wfe['WFS aliasing'] = rad2nm(0.07*dsubr053)

        # Servo-lag errors
        ff = np.pi*0.5**2 # to account for the loss of valid actuator outside the pupil
        nMax = int(np.sqrt(ff)*(self.dms.nControlledRadialOrder[0]+1))
        if hasattr(self.rtc,'ttloop') and self.tts !=None :
            nMin = 3
        else:
            nMin = 1

        nrad  = np.array(range(nMin,nMax))
        self.wfe['HO Servo-lag'] = rad2nm(0.04 * (self.atm.meanWind/self.tel.D/self.rtc.holoop['bandwidth'])\
                                    * Dr053 * np.sum((nrad+1)**(-2/3)))

        # Noise errors
        if self.wfs.processing.noiseVar == [None]:
            varNoise = self.wfs.NoiseVariance(self.atm.r0 ,self.atm.wvl)
        else:
            varNoise = self.wfs.processing.noiseVar

        self.wfe['HO Noise'] = rad2nm(np.mean(varNoise))

        if hasattr(self.rtc,'ttloop') and self.tts !=None:
            self.wfe['TT Servo-lag'] = rad2nm(0.04 * (self.atm.meanWind/self.tel.D/self.rtc.ttloop['bandwidth'])* Dr053 * 2**(-2/3))

            if self.tts.processing.noiseVar == [None]:
                #varNoise = self.tts.NoiseVariance(self.atm.r0 ,self.atm.wvl) # this line has been commented because it makes no sense
                                                                              # to consider the open loop r0 in this computation:
                                                                              # we expect the LO WFS to use a spot corrected by HO loop
                varNoise = self.tts.NoiseVariance(self.tts.optics[0].dsub ,self.atm.wvl)
            else:
                varNoise = self.tts.processing.noiseVar

            self.wfe['TT Noise']     = rad2nm(np.mean(varNoise))
        else:
            self.wfe['TT Servo-lag'] = 0
            self.wfe['TT Noise'] = 0

        # Focal anisoplanatism
        if self.lgs and self.lgs.height[0] > 0:
            self.wfe['Focal anisoplanatism'] = anisoplanatismModel.focal_anisoplanatism_variance(self.tel,self.atm,self.lgs)
        else:
            self.wfe['Focal anisoplanatism'] = 0

        # TO be added : angular anisoplanatisms

        self.wfe['Total'] = np.sqrt(self.wfe['DM fitting']**2 + self.wfe['WFS aliasing']**2\
                                    + self.wfe['HO Servo-lag']**2 + self.wfe['HO Noise']**2\
                                    + self.wfe['TT Servo-lag']**2 + self.wfe['TT Noise']**2\
                                    + self.wfe['Focal anisoplanatism']**2)
        self.wfe['Strehl'] = np.exp(-self.wfe['Total']**2 * (2*np.pi*1e-9/self.src.wvl[0])**2)

