"""
    Copyright (c) 2019 Lumerical Inc. """

######## IMPORTS ########
# General purpose imports
import os
import scipy as sp

## We try to import autograd (and use it's numpy wrapper) but fall back to numpy if autograd is not available
try:
    import autograd.numpy as np
except ImportError:
    import numpy as np

# Optimization specific imports
from lumopt.utilities.load_lumerical_scripts import load_from_lsf
from lumopt.utilities.wavelengths import Wavelengths
from lumopt.geometries.polygon import FunctionDefinedPolygon
from lumopt.figures_of_merit.modematch import ModeMatch
from lumopt.optimizers.generic_optimizers import ScipyOptimizers
from lumopt.optimization import Optimization
from lumopt.utilities.materials import Material

from numpy.random import rand

def runGratingOptimization(bandwidth_in_nm, etch_depth, n_grates, initial_params, min_feature_size, working_dir):
    ### Yet another parametrization which allows to enforce minimum feature size when the optimizer only supports box constraints  
    ### params = [x0, a1, b1, ..., aN]
    if initial_params is None:
        params = np.zeros(2*n_grates)

        for i in range(n_grates):
            params[i*2]   = 0.2     #< Width up
            params[i*2+1] = 0.4*((i+1)/(n_grates+1))     #< Width of the deep etch

        params[0] = 0      #< Overwrite the first since it has a special meaning: Start of the grating at x=0
    else:
        params = initial_params

    bounds = [(min_feature_size, 1)]*(2*n_grates)  
    bounds[0] = (-3,3)      #< Bounds for the stating position

    def grating_params_pos(params, output_waveguide_length = 0.5e-6, height = 220e-9, y0 = 0):
        x_begin = -5.1e-6
        y3 = y0+height
        y1 = y3-etch_depth

        x0 = params[0]*1e-6     #< First parameter is the starting position
        verts = np.array( [ [x_begin,y0],[x_begin,y3],[x0,y3],[x0,y1] ] )
        
        ## Iterate over all but the last
        for i in range(n_grates-1):
            x1 = x0 + params[i*2+1]*1e-6    #< Width of the etch
            x2 = x1 + params[i*2+2]*1e-6    #< Width up
            verts = np.concatenate((verts,np.array([[x1,y1],[x1,y3],[x2,y3],[x2,y1]])),axis=0)
            x0 = x2

        x1 = x0 + params[(n_grates-1)*2+1]*1e-6    #< Width of the etch
        x_end   = x1+output_waveguide_length
        verts = np.concatenate((verts,np.array([[x1,y1],[x1,y3],[x_end,y3],[x_end,y0]])),axis=0) 

        return verts

    geometry = FunctionDefinedPolygon(func = grating_params_pos, initial_params = params, bounds = bounds, z = 0.0, depth = 220e-9, eps_out = 1.44 ** 2, eps_in = 3.47668 ** 2, edge_precision = 5, dx = 1e-5)

    ######## DEFINE FIGURE OF MERIT ########
    fom = ModeMatch(monitor_name = 'fom', mode_number = 1, direction = 'Backward', target_T_fwd = lambda wl: np.ones(wl.size), norm_p = 1)

    ######## DEFINE OPTIMIZATION ALGORITHM ########
    optimizer = ScipyOptimizers(max_iter = 250, method = 'L-BFGS-B', scaling_factor = 1, pgtol = 1e-6)

    ######## DEFINE BASE SIMULATION ########
    base_script = load_from_lsf(os.path.join(os.path.dirname(__file__), 'grating_coupler_2D_TE_base.lsf'))

    ######## PUT EVERYTHING TOGETHER ########
    lambda_start = 1550 - bandwidth_in_nm/2
    lambda_end   = 1550 + bandwidth_in_nm/2
    lambda_pts   = int(bandwidth_in_nm/10)+1
    wavelengths = Wavelengths(start = lambda_start*1e-9, stop = lambda_end*1e-9, points = lambda_pts)
    opt = Optimization(base_script = base_script, wavelengths = wavelengths, fom = fom, geometry = geometry, optimizer = optimizer, hide_fdtd_cad = False, use_deps = True)

    ######## RUN THE OPTIMIZER ########
    opt.run(working_dir=working_dir)

if __name__ == "__main__":
    bandwidth_in_nm = 0
    etch_depth =80          #< Etch depth in nm
    min_feature_size = 0.0  #< Minimal feature size in um. Set to 0.1 to enforce 100nm min features size!

    # An initial guess for etch_depth of 80nm (efficiency around 62.4%) obtained via unconstraint optimization of an apodized grating
    # Data extracted by running the script 'extract_grating_parameters_from_poly.lsf' on the result of an optimized apodized grating.
    # Original efficiency is around 61%, end result should be 63.3% (unconstrained) or    
    initial_params = [-2.54403, 0.02892, 0.54957, 0.04342, 0.53651, 0.05802, 0.52336, 0.072742, 0.510108, 0.08756,
                       0.49675, 0.10250, 0.48329, 0.11756, 0.46973, 0.13273, 0.45607, 0.148019, 0.442304, 0.16342,
                       0.42842, 0.17895, 0.41444, 0.19459, 0.40035, 0.21036, 0.38614, 0.226264, 0.371826, 0.24228,
                       0.35739, 0.25843, 0.34284, 0.27471, 0.32818, 0.29113, 0.31339, 0.307678, 0.298494, 0.32436,
                       0.28346, 0.34118, 0.26831, 0.35813, 0.25304, 0.37523, 0.23764, 0.392482, 0.222109, 0.40987]

    # Result of the unconstrained optimization (efficiency around 63.6%) which can be used as a starting point for a constrained optimization
    # min_feature_size = 0.1  #< Minimal feature size in um. Set to 0.1 to enforce 100nm min features size!
    # initial_params = [-2.55955425, 0.04524479, 0.55014607, 0.05771275, 0.53619054, 0.06678817, 0.50813073, 0.0762642, 0.49318771, 0.0882054,
    #                    0.49669048, 0.09924192, 0.49189945, 0.11347001, 0.48208261, 0.12614727, 0.45446162, 0.1816779, 0.39805665, 0.2245512,
    #                    0.36282676, 0.25205146, 0.35629164, 0.24557389, 0.35970783, 0.24295631, 0.36302675, 0.2407215, 0.37556446, 0.2285697,
    #                    0.37318002, 0.22560285, 0.38048062, 0.21404203, 0.38277723, 0.21511645, 0.37510068, 0.2375193, 0.35612741, 0.2687912,
    #                    0.32171071, 0.32198251, 0.28856622, 0.36277375, 0.26420341, 0.3793884,  0.23229422, 0.3645181, 0.17062863, 0.3279890]

    cur_path = os.path.dirname(os.path.realpath(__file__))
    working_dir = os.path.join(cur_path,'FreeGrating_1etch')

    runGratingOptimization(bandwidth_in_nm=bandwidth_in_nm,
                           etch_depth=etch_depth*1e-9,
                           n_grates = 25,
                           initial_params=initial_params,
                           min_feature_size=min_feature_size, 
                           working_dir=working_dir)
