####################################################################################
#
# Module for cubed-sphere mesh generation and structuring.
#
# Based on "C. Ronchi, R. Iacono, P.S. Paolucci, The “Cubed Sphere”:
# A New Method for the Solution of Partial Differential Equations
# in Spherical Geometry."
#
# Luan da Fonseca Santos - January 2022
# (luan.santos@usp.br)
####################################################################################

import numpy as np
from constants import*
from cs_transform import*
from sphgeo import point, tg_vector_geodesic_pu_midpoints, tg_vector_geodesic_pv_midpoints
import time
import os.path
import netCDF4 as nc

####################################################################################
#  Global cubed-sphere grid structure
####################################################################################
class cubed_sphere:
    def __init__(self, N, transformation, showonscreen, gridload):
        # Panel indexes distribution
        #      +---+
        #      | 4 |
        #  +---+---+---+---+
        #  | 3 | 0 | 1 | 2 |
        #  +---+---+---+---+
        #      | 5 |
        #      +---+

        # Sphere radius
        #self.R = erad
        self.R = 1.0
        self.projection = transformation
        self.N = N
        self.name = self.projection+"_cs_"+str(N)
        gengrid = not gridload

        # Grid data netcdf filename
        self.netcdfdata_filename = griddir+self.name+".nc"
        if gridload:
            # Check if grid file exists
            if not os.path.isfile(self.netcdfdata_filename):
                if self.projection=='overlapped':
                    print('ERROR in cubed_sphere: couldnt find the grid file to be read: ', self.netcdfdata_filename)
                    exit()
                elif self.projection=='gnomonic_equidistant' or self.projection=='gnomonic_equiangular':
                    print('Couldnt find the grid file to be read: ', self.netcdfdata_filename)
                    gengrid=True
            else: # Read from netcdf file
                if showonscreen:
                    print("--------------------------------------------------------")
                    print('Loading grid file '+self.netcdfdata_filename)

                # Start time counting
                start_time = time.time()

                # Open grid data
                griddata = nc.Dataset(self.netcdfdata_filename,'r')

                # Grid spacing
                dx = griddata['dx'][:]
                dy = griddata['dy'][:]
                self.dx, self.dy = dx, dy

                self.N = griddata['N'][:]
                self.a = griddata['a'][:] # 1d grid is defined on [-a,a]

                # Get ghost cell numbers
                self.ng  = griddata['ng'][:]
                self.ngl = griddata['ngl'][:]
                self.ngr = griddata['ngr'][:]

                # Interior indexes
                self.i0, self.iend = griddata['i0'][:], griddata['iend'][:]
                self.j0, self.jend = griddata['j0'][:], griddata['jend'][:]
                i0, iend = self.i0, self.iend
                j0, jend = self.j0, self.jend
                ng = self.ng

                # Create points
                po    = point(N+1+ng, N+1+ng)
                pc    = point(N+ng, N+ng)
                pu    = point(N+1+ng, N+ng)
                pv    = point(N+ng, N+1+ng)
                ex_po = point(N+ng+1, N+ng+1)
                ey_po = point(N+ng+1, N+ng+1)
                ex_pc = point(N+ng, N+ng)
                ey_pc = point(N+ng, N+ng)
                ex_pu = point(N+1+ng, N+ng)
                ey_pu = point(N+1+ng, N+ng)
                ex_pv = point(N+ng, N+1+ng)
                ey_pv = point(N+ng, N+1+ng)

                # Get values from file
                po.X   = griddata['po'][:,:,:,0]
                po.Y   = griddata['po'][:,:,:,1]
                po.Z   = griddata['po'][:,:,:,2]
                po.lon = griddata['po'][:,:,:,3]
                po.lat = griddata['po'][:,:,:,4]

                pc.X   = griddata['pc'][:,:,:,0]
                pc.Y   = griddata['pc'][:,:,:,1]
                pc.Z   = griddata['pc'][:,:,:,2]
                pc.lon = griddata['pc'][:,:,:,3]
                pc.lat = griddata['pc'][:,:,:,4]

                pu.X   = griddata['pu'][:,:,:,0]
                pu.Y   = griddata['pu'][:,:,:,1]
                pu.Z   = griddata['pu'][:,:,:,2]
                pu.lon = griddata['pu'][:,:,:,3]
                pu.lat = griddata['pu'][:,:,:,4]

                pv.X   = griddata['pv'][:,:,:,0]
                pv.Y   = griddata['pv'][:,:,:,1]
                pv.Z   = griddata['pv'][:,:,:,2]
                pv.lon = griddata['pv'][:,:,:,3]
                pv.lat = griddata['pv'][:,:,:,4]

                ex_po.X   = griddata['ex_po'][:,:,:,0]
                ex_po.Y   = griddata['ex_po'][:,:,:,1]
                ex_po.Z   = griddata['ex_po'][:,:,:,2]
                ex_po.lon = griddata['ex_po'][:,:,:,3]
                ex_po.lat = griddata['ex_po'][:,:,:,4]

                ey_po.X   = griddata['ey_po'][:,:,:,0]
                ey_po.Y   = griddata['ey_po'][:,:,:,1]
                ey_po.Z   = griddata['ey_po'][:,:,:,2]
                ey_po.lon = griddata['ey_po'][:,:,:,3]
                ey_po.lat = griddata['ey_po'][:,:,:,4]

                ex_pc.X   = griddata['ex_pc'][:,:,:,0]
                ex_pc.Y   = griddata['ex_pc'][:,:,:,1]
                ex_pc.Z   = griddata['ex_pc'][:,:,:,2]
                ex_pc.lon = griddata['ex_pc'][:,:,:,3]
                ex_pc.lat = griddata['ex_pc'][:,:,:,4]

                ey_pc.X   = griddata['ey_pc'][:,:,:,0]
                ey_pc.Y   = griddata['ey_pc'][:,:,:,1]
                ey_pc.Z   = griddata['ey_pc'][:,:,:,2]
                ey_pc.lon = griddata['ey_pc'][:,:,:,3]
                ey_pc.lat = griddata['ey_pc'][:,:,:,4]

                ex_pu.X   = griddata['ex_pu'][:,:,:,0]
                ex_pu.Y   = griddata['ex_pu'][:,:,:,1]
                ex_pu.Z   = griddata['ex_pu'][:,:,:,2]
                ex_pu.lon = griddata['ex_pu'][:,:,:,3]
                ex_pu.lat = griddata['ex_pu'][:,:,:,4]

                ey_pu.X   = griddata['ey_pu'][:,:,:,0]
                ey_pu.Y   = griddata['ey_pu'][:,:,:,1]
                ey_pu.Z   = griddata['ey_pu'][:,:,:,2]
                ey_pu.lon = griddata['ey_pu'][:,:,:,3]
                ey_pu.lat = griddata['ey_pu'][:,:,:,4]

                ex_pv.X   = griddata['ex_pv'][:,:,:,0]
                ex_pv.Y   = griddata['ex_pv'][:,:,:,1]
                ex_pv.Z   = griddata['ex_pv'][:,:,:,2]
                ex_pv.lon = griddata['ex_pv'][:,:,:,3]
                ex_pv.lat = griddata['ex_pv'][:,:,:,4]

                ey_pv.X   = griddata['ey_pv'][:,:,:,0]
                ey_pv.Y   = griddata['ey_pv'][:,:,:,1]
                ey_pv.Z   = griddata['ey_pv'][:,:,:,2]
                ey_pv.lon = griddata['ey_pv'][:,:,:,3]
                ey_pv.lat = griddata['ey_pv'][:,:,:,4]

                # Attributes
                self.pc = pc
                self.po = po
                self.pu = pu
                self.pv = pv

                self.ex_po = ex_po
                self.ey_po = ey_po
                self.ex_pc = ex_pc
                self.ey_pc = ey_pc
                self.ex_pu = ex_pu
                self.ey_pu = ey_pu
                self.ex_pv = ex_pv
                self.ey_pv = ey_pv

                # Close netcdf file
                griddata.close()

                # Finish time counting
                elapsed_time = time.time() - start_time

                # Latlon/Contravariant conversion
                if showonscreen:
                    print("Done.\n")

        if gengrid:
            # Generate the grid
            if showonscreen:
                print("--------------------------------------------------------")
                print("Generating "+self.projection+" cubed-sphere with "+str(nbfaces*N*N)+" cells...")

            # 1D grid extremes
            if transformation == "gnomonic_equiangular":
                a = pio4
            elif transformation == "gnomonic_equidistant":
                a = self.R/np.sqrt(3.0)  # Half length of the cube
            elif transformation == "conformal":
                a = 1.0
            else:
                print("ERROR: invalid grid transformation.")
                exit()
            self.a = a
            x_min, x_max, y_min, y_max = [-a,a,-a,a]

            # Start time counting
            start_time = time.time()

            # Grid spacing
            dx = (x_max-x_min)/N
            dy = (y_max-y_min)/N
            self.dx, self.dy = dx, dy

            # Ghost cells size
            ngl = 4
            ngr = 4

            ng = ngr + ngl
            self.ngl  = ngl
            self.ngr = ngr
            self.ng = ng

            # Interior indexes
            i0, iend = ngl, ngl+N
            j0, jend = ngl, ngl+N
            self.i0, self.iend = ngl, ngl+N
            self.j0, self.jend = ngl, ngl+N

            # Generate cell po
            x_po = np.linspace(x_min-ngl*dx, x_max + ngr*dx, N+1+ng) # po
            y_po = np.linspace(y_min-ngl*dx, y_max + ngr*dy, N+1+ng) # po
            if showonscreen:
                print("Generating cell po...")

            po = point(N+1+ng, N+1+ng)
            ex_po = point(N+1+ng, N+1+ng)
            ey_po = point(N+1+ng, N+1+ng)

            if transformation == "gnomonic_equiangular":
                po.X, po.Y, po.Z, po.lon, po.lat = equiangular_gnomonic_map(x_po, y_po, N+1+ng, N+1+ng, self.R)
                ex_po.X, ex_po.Y, ex_po.Z = equiangular_tg_xdir(x_po, y_po, N+ng+1, N+ng+1, self.R)
                ey_po.X, ey_po.Y, ey_po.Z = equiangular_tg_ydir(x_po, y_po, N+ng+1, N+ng+1, self.R)

            elif transformation=="gnomonic_equidistant":
                po.X, po.Y, po.Z, po.lon, po.lat = equidistant_gnomonic_map(x_po, y_po, N+1+ng, N+1+ng, self.R)
                ex_po.X, ex_po.Y, ex_po.Z = equidistant_tg_xdir(x_po, y_po, N+ng+1, N+ng+1, self.R)
                ey_po.X, ey_po.Y, ey_po.Z = equidistant_tg_ydir(x_po, y_po, N+ng+1, N+ng+1, self.R)

            #elif transformation=="conformal":
            #    po.X[i0:iend+1,j0:jend+1], po.Y[i0:iend+1,j0:jend+1], po.Z[i0:iend+1,j0:jend+1], po.lon[i0:iend+1,j0:jend+1], po.lat[i0:iend+1,j0:jend+1] = conformal_map(x_po[i0:iend+1], y_po[j0:jend+1], N+1, N+1)

            self.po = po

            # Generate cell pc
            if showonscreen:
                print("Generating cell pc...")

            pc = point(N+ng, N+ng)
            ex_pc   = point(N+ng, N+ng)
            ey_pc   = point(N+ng, N+ng)

            x_pc = np.linspace(x_min+dx/2.0-ngl*dx, x_max-dx/2.0+ngr*dx, N+ng) # Centers
            y_pc = np.linspace(y_min+dy/2.0-ngl*dy, y_max-dy/2.0+ngr*dy, N+ng) # Centers

            if transformation == "gnomonic_equiangular":
                pc.X, pc.Y, pc.Z, pc.lon, pc.lat = equiangular_gnomonic_map(x_pc, y_pc, N+ng, N+ng, self.R)
                ex_pc.X, ex_pc.Y, ex_pc.Z = equiangular_tg_xdir(x_pc, y_pc, N+ng, N+ng, self.R)
                ey_pc.X, ey_pc.Y, ey_pc.Z = equiangular_tg_ydir(x_pc, y_pc, N+ng, N+ng, self.R)

            elif transformation=="gnomonic_equidistant":
                pc.X, pc.Y, pc.Z, pc.lon, pc.lat = equidistant_gnomonic_map(x_pc, y_pc, N+ng, N+ng, self.R)
                ex_pc.X, ex_pc.Y, ex_pc.Z = equidistant_tg_xdir(x_pc, y_pc, N+ng, N+ng, self.R)
                ey_pc.X, ey_pc.Y, ey_pc.Z = equidistant_tg_ydir(x_pc, y_pc, N+ng, N+ng, self.R)
            #elif transformation=="conformal":
            #    pc.X[i0:iend,j0:jend], pc.Y[i0:iend,j0:jend], pc.Z[i0:iend,j0:jend], pc.lon[i0:iend,j0:jend], pc.lat[i0:iend,j0:jend] \
            #    = conformal_map(x_pc[i0:iend], y_pc[j0:jend], N, N)

            self.pc = pc
            self.ex_pc = ex_pc
            self.ey_pc = ey_pc

            # Generate cell edges in x direction
            if showonscreen:
                print("Generating cell edges and tangent vectors in x direction...")

            pu    = point(N+1+ng, N+ng)
            ex_pu = point(N+1+ng, N+ng)
            ey_pu = point(N+1+ng, N+ng)
            x_pu = np.linspace(x_min-ngl*dx, x_max+ngr*dx, N+1+ng) # Edges
            y_pu = np.linspace(y_min+dy/2.0-ngl*dy, y_max-dy/2.0+ngr*dy, N+ng) # Centers

            if transformation == "gnomonic_equiangular":
                pu.X, pu.Y, pu.Z, pu.lon, pu.lat = equiangular_gnomonic_map(x_pu, y_pu, N+1+ng, N+ng, self.R)
                ex_pu.X, ex_pu.Y, ex_pu.Z = equiangular_tg_xdir(x_pu, y_pu, N+1+ng, N+ng, self.R)
                ey_pu.X, ey_pu.Y, ey_pu.Z = equiangular_tg_ydir(x_pu, y_pu, N+1+ng, N+ng, self.R)
            elif transformation=="gnomonic_equidistant":
                pu.X, pu.Y, pu.Z, pu.lon, pu.lat = equidistant_gnomonic_map(x_pu, y_pu, N+1+ng, N+ng, self.R)
                ex_pu.X, ex_pu.Y, ex_pu.Z = equidistant_tg_xdir(x_pu, y_pu, N+1+ng, N+ng, self.R)
                ey_pu.X, ey_pu.Y, ey_pu.Z = equidistant_tg_ydir(x_pu, y_pu, N+1+ng, N+ng, self.R)

            #elif transformation=="conformal":
            #    pu.X[i0:iend+1,j0:jend], pu.Y[i0:iend+1,j0:jend], pu.Z[i0:iend+1,j0:jend], pu.lon[i0:iend+1,j0:jend], pu.lat[i0:iend+1,j0:jend] \
            #    = conformal_map(x_pu[i0:iend+1], y_pu[j0:jend], N+1, N)

            self.pu = pu

            # Generate cell edges in y direction
            if showonscreen:
                print("Generating cell edges and tangent vectors in y direction...")

            pv = point(N+ng, N+ng+1)
            ex_pv = point(N+ng, N+ng+1)
            ey_pv = point(N+ng, N+ng+1)
            x_pv = np.linspace(x_min+dx/2.0-ngl*dx, x_max-dx/2.0+ngr*dx, N+ng) # Centers
            y_pv = np.linspace(y_min-ngl*dx, y_max + ngr*dy, N+1+ng) # Edges

            if transformation == "gnomonic_equiangular":
                pv.X, pv.Y, pv.Z, pv.lon, pv.lat = equiangular_gnomonic_map(x_pv, y_pv, N+ng, N+ng+1, self.R)
                ex_pv.X, ex_pv.Y, ex_pv.Z = equiangular_tg_xdir(x_pv, y_pv, N+ng, N+ng+1, self.R)
                ey_pv.X, ey_pv.Y, ey_pv.Z = equiangular_tg_ydir(x_pv, y_pv, N+ng, N+ng+1, self.R)
            elif transformation=="gnomonic_equidistant":
                pv.X, pv.Y, pv.Z, pv.lon, pv.lat = equidistant_gnomonic_map(x_pv, y_pv, N+ng, N+ng+1, self.R)
                ex_pv.X, ex_pv.Y, ex_pv.Z = equidistant_tg_xdir(x_pv, y_pv, N+ng, N+ng+1, self.R)
                ey_pv.X, ey_pv.Y, ey_pv.Z = equidistant_tg_ydir(x_pv, y_pv, N+ng, N+ng+1, self.R)
            #elif transformation=="conformal":
            #    pv.X[i0:iend,j0:jend+1], pv.Y[i0:iend,j0:jend+1], pv.Z[i0:iend,j0:jend+1], pv.lon[i0:iend,j0:jend+1], pv.lat[i0:iend,j0:jend+1] \
            #    = conformal_map(x_pv[i0:iend], y_pv[j0:jend+1], N, N+1)

            self.pv = pv

            # Generate tangent vectors
            if showonscreen:
                print("Generating latlon tangent vectors...")

            # Lat-lon tangent unit vectors
            elon_po = sphgeo.tangent_geo_lon(po.lon)
            elat_po = sphgeo.tangent_geo_lat(po.lon, po.lat)

            # CS map tangent unit vectors at edges points
            # latlon coordinates
            ex_po.lon = ex_po.X[:,:,:]*elon_po[:,:,:,0] + ex_po.Y[:,:,:]*elon_po[:,:,:,1] + ex_po.Z[:,:,:]*elon_po[:,:,:,2]
            ex_po.lat = ex_po.X[:,:,:]*elat_po[:,:,:,0] + ex_po.Y[:,:,:]*elat_po[:,:,:,1] + ex_po.Z[:,:,:]*elat_po[:,:,:,2]

            ey_po.lon = ey_po.X[:,:,:]*elon_po[:,:,:,0] + ey_po.Y[:,:,:]*elon_po[:,:,:,1] + ey_po.Z[:,:,:]*elon_po[:,:,:,2]
            ey_po.lat = ey_po.X[:,:,:]*elat_po[:,:,:,0] + ey_po.Y[:,:,:]*elat_po[:,:,:,1] + ey_po.Z[:,:,:]*elat_po[:,:,:,2]

            self.ex_po = ex_po
            self.ey_po = ey_po

            # Lat-lon tangent unit vectors
            elon_pc = sphgeo.tangent_geo_lon(pc.lon)
            elat_pc = sphgeo.tangent_geo_lat(pc.lon, pc.lat)

            # latlon coordinates
            ex_pc.lon = ex_pc.X[:,:,:]*elon_pc[:,:,:,0] + ex_pc.Y[:,:,:]*elon_pc[:,:,:,1] + ex_pc.Z[:,:,:]*elon_pc[:,:,:,2]
            ex_pc.lat = ex_pc.X[:,:,:]*elat_pc[:,:,:,0] + ex_pc.Y[:,:,:]*elat_pc[:,:,:,1] + ex_pc.Z[:,:,:]*elat_pc[:,:,:,2]

            ey_pc.lon = ey_pc.X[:,:,:]*elon_pc[:,:,:,0] + ey_pc.Y[:,:,:]*elon_pc[:,:,:,1] + ey_pc.Z[:,:,:]*elon_pc[:,:,:,2]
            ey_pc.lat = ey_pc.X[:,:,:]*elat_pc[:,:,:,0] + ey_pc.Y[:,:,:]*elat_pc[:,:,:,1] + ey_pc.Z[:,:,:]*elat_pc[:,:,:,2]

            self.ex_pc = ex_pc
            self.ey_pc = ey_pc

            # Lat-lon tangent unit vectors
            elon_pu = sphgeo.tangent_geo_lon(pu.lon)
            elat_pu = sphgeo.tangent_geo_lat(pu.lon, pu.lat)
            elon_pv = sphgeo.tangent_geo_lon(pv.lon)
            elat_pv = sphgeo.tangent_geo_lat(pv.lon, pv.lat)

            # CS map tangent unit vectors at edges points
            # latlon coordinates
            ex_pu.lon = ex_pu.X[:,:,:]*elon_pu[:,:,:,0] + ex_pu.Y[:,:,:]*elon_pu[:,:,:,1] + ex_pu.Z[:,:,:]*elon_pu[:,:,:,2]
            ex_pu.lat = ex_pu.X[:,:,:]*elat_pu[:,:,:,0] + ex_pu.Y[:,:,:]*elat_pu[:,:,:,1] + ex_pu.Z[:,:,:]*elat_pu[:,:,:,2]

            ex_pv.lon = ex_pv.X[:,:,:]*elon_pv[:,:,:,0] + ex_pv.Y[:,:,:]*elon_pv[:,:,:,1] + ex_pv.Z[:,:,:]*elon_pv[:,:,:,2]
            ex_pv.lat = ex_pv.X[:,:,:]*elat_pv[:,:,:,0] + ex_pv.Y[:,:,:]*elat_pv[:,:,:,1] + ex_pv.Z[:,:,:]*elat_pv[:,:,:,2]

            ey_pu.lon = ey_pu.X[:,:,:]*elon_pu[:,:,:,0] + ey_pu.Y[:,:,:]*elon_pu[:,:,:,1] + ey_pu.Z[:,:,:]*elon_pu[:,:,:,2]
            ey_pu.lat = ey_pu.X[:,:,:]*elat_pu[:,:,:,0] + ey_pu.Y[:,:,:]*elat_pu[:,:,:,1] + ey_pu.Z[:,:,:]*elat_pu[:,:,:,2]

            ey_pv.lon = ey_pv.X[:,:,:]*elon_pv[:,:,:,0] + ey_pv.Y[:,:,:]*elon_pv[:,:,:,1] + ey_pv.Z[:,:,:]*elon_pv[:,:,:,2]
            ey_pv.lat = ey_pv.X[:,:,:]*elat_pv[:,:,:,0] + ey_pv.Y[:,:,:]*elat_pv[:,:,:,1] + ey_pv.Z[:,:,:]*elat_pv[:,:,:,2]

            self.ex_pu = ex_pu
            self.ey_pu = ey_pu
            self.ex_pv = ex_pv
            self.ey_pv = ey_pv

        #-------------------------------------------------------------------------------
        # Latlon/Contravariant conversion
        if showonscreen:
            print("Computing metric tensor and latlon/contravariant conversion matrices... ")

        x_min, x_max, y_min, y_max = [-self.a, self.a, -self.a, self.a]
        ng, ngl, ngr = self.ng, self.ngl, self.ngr

        # Metric tensor on pc
        x_pc = np.linspace(x_min+dx/2.0-ngl*dx, x_max-dx/2.0+ngr*dx, N+ng) # Centers
        y_pc = np.linspace(y_min+dy/2.0-ngl*dy, y_max-dy/2.0+ngr*dy, N+ng) # Centers
        metric_tensor_pc = np.zeros((N+ng, N+ng, nbfaces))
        #metric_tensor_pc[:,:,0] = metric_tensor(x_pc, y_pc, self.R, transformation)
        metric_tensor_pc[:,:,0] = \
        -(ex_pc.X[:,:,0]*ey_pc.X[:,:,0]+ex_pc.Y[:,:,0]*ey_pc.Y[:,:,0]+ex_pc.Z[:,:,0]*ey_pc.Z[:,:,0])**2\
        +(ex_pc.X[:,:,0]**2 + ex_pc.Y[:,:,0]**2+ ex_pc.Z[:,:,0]**2)*\
         (ey_pc.X[:,:,0]**2 + ey_pc.Y[:,:,0]**2+ ey_pc.Z[:,:,0]**2)
        metric_tensor_pc[:,:,0] = np.sqrt(metric_tensor_pc[:,:,0])
        for p in range(1, nbfaces): metric_tensor_pc[:,:,p] = metric_tensor_pc[:,:,0]
        self.metric_tensor_pc = metric_tensor_pc

        # Metric tensor on edges in x direction
        x_pu = np.linspace(x_min-ngl*dx, x_max+ngr*dx, N+1+ng) # Edges
        y_pu = np.linspace(y_min+dy/2.0-ngl*dy, y_max-dy/2.0+ngr*dy, N+ng) # Centers
        metric_tensor_pu = np.zeros((N+1+ng, N+ng, nbfaces))
        #metric_tensor_pu[:,:,0] = metric_tensor(x_pu, y_pu, self.R, transformation)
        metric_tensor_pu[:,:,0] = \
        -(ex_pu.X[:,:,0]*ey_pu.X[:,:,0]+ex_pu.Y[:,:,0]*ey_pu.Y[:,:,0]+ex_pu.Z[:,:,0]*ey_pu.Z[:,:,0])**2\
        +(ex_pu.X[:,:,0]**2 + ex_pu.Y[:,:,0]**2+ ex_pu.Z[:,:,0]**2)*\
         (ey_pu.X[:,:,0]**2 + ey_pu.Y[:,:,0]**2+ ey_pu.Z[:,:,0]**2)
        metric_tensor_pu[:,:,0] = np.sqrt(metric_tensor_pu[:,:,0])
        for p in range(1, nbfaces): metric_tensor_pu[:,:,p] = metric_tensor_pu[:,:,0]
        self.metric_tensor_pu = metric_tensor_pu
        Xu, Yu = np.meshgrid(x_pu, y_pu,indexing='ij')

        # Metric tensor on edges in y direction
        x_pv = np.linspace(x_min+dx/2.0-ngl*dx, x_max-dx/2.0+ngr*dx, N+ng) # Centers
        y_pv = np.linspace(y_min-ngl*dx, y_max + ngr*dy, N+1+ng) # Edges
        Xv, Yv = np.meshgrid(x_pv, y_pv,indexing='ij')
        metric_tensor_pv = np.zeros((N+ng, N+ng+1, nbfaces))
        #metric_tensor_pv[:,:,0] = metric_tensor(x_pv, y_pv, self.R, transformation)
        metric_tensor_pv[:,:,0] = \
        -(ex_pv.X[:,:,0]*ey_pv.X[:,:,0]+ex_pv.Y[:,:,0]*ey_pv.Y[:,:,0]+ex_pv.Z[:,:,0]*ey_pv.Z[:,:,0])**2\
        +(ex_pv.X[:,:,0]**2 + ex_pv.Y[:,:,0]**2+ ex_pv.Z[:,:,0]**2)*\
         (ey_pv.X[:,:,0]**2 + ey_pv.Y[:,:,0]**2+ ey_pv.Z[:,:,0]**2)
        metric_tensor_pv[:,:,0] = np.sqrt(metric_tensor_pv[:,:,0])
        for p in range(1, nbfaces): metric_tensor_pv[:,:,p] = metric_tensor_pv[:,:,0]
        self.metric_tensor_pv = metric_tensor_pv

        # Lat-lon tangent unit vectors
        elon_pc = sphgeo.tangent_geo_lon(pc.lon)
        elat_pc = sphgeo.tangent_geo_lat(pc.lon, pc.lat)
        elon_pu = sphgeo.tangent_geo_lon(pu.lon)
        elat_pu = sphgeo.tangent_geo_lat(pu.lon, pu.lat)
        elon_pv = sphgeo.tangent_geo_lon(pv.lon)
        elat_pv = sphgeo.tangent_geo_lat(pv.lon, pv.lat)

        # Latlon/Contravariant conversion at pc
        prod_ex_elon_pc = ex_pc.X*elon_pc[:,:,:,0] + ex_pc.Y*elon_pc[:,:,:,1] + ex_pc.Z*elon_pc[:,:,:,2]
        prod_ex_elat_pc = ex_pc.X*elat_pc[:,:,:,0] + ex_pc.Y*elat_pc[:,:,:,1] + ex_pc.Z*elat_pc[:,:,:,2]
        prod_ey_elon_pc = ey_pc.X*elon_pc[:,:,:,0] + ey_pc.Y*elon_pc[:,:,:,1] + ey_pc.Z*elon_pc[:,:,:,2]
        prod_ey_elat_pc = ey_pc.X*elat_pc[:,:,:,0] + ey_pc.Y*elat_pc[:,:,:,1] + ey_pc.Z*elat_pc[:,:,:,2]
        determinant_ll2contra_pc = prod_ex_elon_pc*prod_ey_elat_pc - prod_ey_elon_pc*prod_ex_elat_pc

        self.prod_ex_elon_pc = prod_ex_elon_pc
        self.prod_ex_elat_pc = prod_ex_elat_pc
        self.prod_ey_elon_pc = prod_ey_elon_pc
        self.prod_ey_elat_pc = prod_ey_elat_pc
        self.determinant_ll2contra_pc = determinant_ll2contra_pc

        # Latlon/Contravariant conversion at pu
        prod_ex_elon_pu = ex_pu.X*elon_pu[:,:,:,0] + ex_pu.Y*elon_pu[:,:,:,1] + ex_pu.Z*elon_pu[:,:,:,2]
        prod_ex_elat_pu = ex_pu.X*elat_pu[:,:,:,0] + ex_pu.Y*elat_pu[:,:,:,1] + ex_pu.Z*elat_pu[:,:,:,2]
        prod_ey_elon_pu = ey_pu.X*elon_pu[:,:,:,0] + ey_pu.Y*elon_pu[:,:,:,1] + ey_pu.Z*elon_pu[:,:,:,2]
        prod_ey_elat_pu = ey_pu.X*elat_pu[:,:,:,0] + ey_pu.Y*elat_pu[:,:,:,1] + ey_pu.Z*elat_pu[:,:,:,2]
        determinant_ll2contra_pu = prod_ex_elon_pu*prod_ey_elat_pu - prod_ey_elon_pu*prod_ex_elat_pu

        self.prod_ex_elon_pu = prod_ex_elon_pu
        self.prod_ex_elat_pu = prod_ex_elat_pu
        self.prod_ey_elon_pu = prod_ey_elon_pu
        self.prod_ey_elat_pu = prod_ey_elat_pu
        self.determinant_ll2contra_pu = determinant_ll2contra_pu

        # Latlon/Contravariant conversion at pv
        prod_ex_elon_pv = ex_pv.X*elon_pv[:,:,:,0] + ex_pv.Y*elon_pv[:,:,:,1] + ex_pv.Z*elon_pv[:,:,:,2]
        prod_ex_elat_pv = ex_pv.X*elat_pv[:,:,:,0] + ex_pv.Y*elat_pv[:,:,:,1] + ex_pv.Z*elat_pv[:,:,:,2]
        prod_ey_elon_pv = ey_pv.X*elon_pv[:,:,:,0] + ey_pv.Y*elon_pv[:,:,:,1] + ey_pv.Z*elon_pv[:,:,:,2]
        prod_ey_elat_pv = ey_pv.X*elat_pv[:,:,:,0] + ey_pv.Y*elat_pv[:,:,:,1] + ey_pv.Z*elat_pv[:,:,:,2]
        determinant_ll2contra_pv = prod_ex_elon_pv*prod_ey_elat_pv - prod_ey_elon_pv*prod_ex_elat_pv

        self.prod_ex_elon_pv = prod_ex_elon_pv
        self.prod_ex_elat_pv = prod_ex_elat_pv
        self.prod_ey_elon_pv = prod_ey_elon_pv
        self.prod_ey_elat_pv = prod_ey_elat_pv
        self.determinant_ll2contra_pv = determinant_ll2contra_pv

        # Latlon/Contravariant conversion
        if showonscreen:
            print("Done.")

        #ex_pu, ey_pu
        #ex_pv, ey_pv
        #self.elon_pu, self.elat_pu
        #self.elon_pv, self.elat_pv

        Xu = np.repeat(Xu[:, :, np.newaxis], 6, axis=2)
        Yv = np.repeat(Yv[:, :, np.newaxis], 6, axis=2)
        self.Xu, self.Yv = Xu, Yv

        # Compute cell lenghts
        # Compute the geodesic distance of cell edges in x direction
        length_pu =  self.R*metric_tensor_pu*dy
        length_pv =  self.R*metric_tensor_pu*dx

        # Compute areas
        areas = (self.R**2)*metric_tensor_pc*dx*dy
        #print(abs(np.pi*4.0 - np.sum(self.areas[i0:iend,j0:jend,:]))/abs(np.pi*4.0) )
        #exit()

        # Finish time counting
        elapsed_time = time.time() - start_time

        if showonscreen:
            # Print some grid properties
            print("\nMin  edge length (km)  : ","{:.2e}".format(np.amin(length_pu[i0:iend+1,j0:jend,:])*erad/10**3))
            print("Max  edge length (km)  : ","{:.2e}".format(np.amax(length_pu[i0:iend+1,j0:jend,:])*erad/10**3))
            print("Mean edge length (km)  : ","{:.2e}".format(np.mean(length_pu[i0:iend+1,j0:jend,:])*erad/10**3))
            print("Ratio max/min length   : ","{:.2e}".format(np.amax(length_pu[i0:iend+1,j0:jend,:])/np.amin(length_pu[i0:iend+1,j0:jend,:])))

            print("Min  area (km2)        : ","{:.2e}".format(np.amin(areas[i0:iend,j0:jend,:])*erad*erad/10**6))
            print("Max  area (km2)        : ","{:.2e}".format(np.amax(areas[i0:iend,j0:jend,:])*erad*erad/10**6))
            print("Mean area (km2)        : ","{:.2e}".format(np.mean(areas[i0:iend,j0:jend,:])*erad*erad/10**6))
            print("Ratio max/min area     : ","{:.2e}".format(np.amax(areas[i0:iend,j0:jend,:])/np.amin(areas[i0:iend,j0:jend,:])))

            #m = N
            #ratio = (1+2*np.tan((pi/4))**2 * (1-1/m))**1.5
            #ratio = ratio*np.cos((pi/4))**4*(1-1/m)
            #print(ratio)
            #exit()

        if showonscreen:
            print("\nDone in ","{:.2e}".format(elapsed_time),"seconds.")
            print("--------------------------------------------------------\n")

####################################################################################
# Structure for scalar values on cubed-sphere grid
####################################################################################
class scalar_field:
    def __init__(self, grid, name, position):
        # Field name
        self.name = name

        # Field position
        self.position = position

        # Number of values in a coordinate axis (same for all panels)
        if position == "vertex":
            self.N = grid.N+1
        elif position == "center":
            self.N = grid.N
        else:
            print("ERROR in scalar_field: invalid position.")
            exit()

        # Field values along a coordinate axis (same for all panels)
        self.f = np.zeros((self.N, self.N, nbfaces))

####################################################################################
# Structure for lat-lon grid
####################################################################################
class latlon_grid:
    def __init__(self, Nlat, Nlon):
        self.Nlat, self.Nlon = Nlat, Nlon

        # Create a latlon grid
        lat = np.linspace(-pio2, pio2, Nlat)
        lon = np.linspace(  -pi,   pi, Nlon)
        self.lat, self.lon = np.meshgrid(lat, lon)

        # Convert to cartesian coordinates
        self.X, self.Y, self.Z = sphgeo.sph2cart(self.lon, self.lat)

        #Lat-lon to cubed sphere indexes
        self.ix, self.jy, self.mask = [], [], []

####################################################################################
# Compute execution time of the grid generation for different resolutions
####################################################################################
def cs_time_test():
    import time
    from matplotlib import pyplot as plt

    # Values of N
    Ns = (1,10,100,1000)

    # Number we generate the grid to compute the execution time
    Ntest = 10

    # Vector that save the execution time for each N
    times = np.zeros(len(Ns))

    # Calculate the average execution time using Ntest executions
    k = 0
    for N in Ns:
        for t in range(0,Ntest):
            start_time = time.time()
            grid = cubed_sphere(N)
            elapsed_time = time.time() - start_time
            times[k] = times[k] + elapsed_time/Ntest
            print(N,t,elapsed_time)
        k = k + 1
    print("N and average time (in seconds):")
    k = 0

    # Show the value of N and execution time on the screen
    for k in range(0,len(Ns)):
        print(Ns[k], times[k])

    print('\n')
    for k in range(1,len(Ns)):
        print(Ns[k], times[k]/times[k-1])

    # Plot the graph in logscale
    plt.figure()
    plt.loglog(Ns, times,marker='x')
    plt.Ylabel('Time (seconds)', fontsize=12)
    plt.Xlabel('$N$', fontsize=12)
    plt.savefig(graphdir+'execution_time'+'.eps', format='eps')
    plt.show()
    plt.close()

####################################################################################
#  Parabola class
####################################################################################
class ppm_parabola:
    def __init__(self, cs_grid, simulation, direction):
        # Number of cells
        N  = cs_grid.N
        M  = N
        ng = cs_grid.ng

        # reconstruction name
        self.recon_name = simulation.recon_name

        # parabola coefficients
        # Notation from Colella and  Woodward 1984
        # q(x) = q_L + z*(dq + q6*(1-z)) z in [0,1]
        self.q_L = np.zeros((N+ng, M+ng, nbfaces))
        self.q_R = np.zeros((N+ng, M+ng, nbfaces))
        self.dq  = np.zeros((N+ng, M+ng, nbfaces))
        self.q6  = np.zeros((N+ng, M+ng, nbfaces))

        if direction == 'x':
            # parabola fluxes
            self.f_L = np.zeros((N+ng+1, M+ng, nbfaces))  # flux from left
            self.f_R = np.zeros((N+ng+1, M+ng, nbfaces))  # flux from right
            self.f_upw = np.zeros((N+ng+1, M+ng, nbfaces)) # upwind flux

            # Extra variables for each scheme
            if simulation.recon_name == 'PPM-0' or simulation.recon_name == 'PPM-CW84' or simulation.recon_name == 'PPM-L04':
                self.Q_edges =  np.zeros((N+ng+1, M+ng, nbfaces))
        elif direction == 'y':
            # parabola fluxes
            self.f_L = np.zeros((N+ng, M+ng+1, nbfaces))   # flux from left
            self.f_R = np.zeros((N+ng, M+ng+1, nbfaces))   # flux from right
            self.f_upw = np.zeros((N+ng, M+ng+1, nbfaces)) # upwind flux

            # Extra variables for each scheme
            if simulation.recon_name == 'PPM-0' or simulation.recon_name == 'PPM-CW84' or simulation.recon_name == 'PPM-L04':
                self.Q_edges =  np.zeros((N+ng, M+ng+1, nbfaces))

        self.dF = np.zeros((N+ng, M+ng, nbfaces)) # div flux

        if simulation.recon_name == 'PPM-CW84':
            self.dQ  = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ0 = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ1 = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ2 = np.zeros((N+ng, M+ng, nbfaces))

        if simulation.recon_name == 'PPM-L04':
            self.dQ      = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ_min  = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ_max  = np.zeros((N+ng, M+ng, nbfaces))
            self.dQ_mono = np.zeros((N+ng, M+ng, nbfaces))

####################################################################################
#  Velocity class
#  The quadrilateral points are labeled as below
#
#  po-------pv--------po
#  |                  |
#  |                  |
#  |                  |
#  pu       pc        pu
#  |                  |
#  |                  |
#  |                  |
#  po--------pv-------po
#
####################################################################################
class velocity:
    def __init__(self, cs_grid, pos):
        N = cs_grid.N
        ng = cs_grid.ng

        if pos == 'pu':
            self.ulon = np.zeros((N+1+ng, N+ng, nbfaces))
            self.vlat = np.zeros((N+1+ng, N+ng, nbfaces))
            self.ucontra = np.zeros((N+1+ng, N+ng, nbfaces))
            self.vcontra = np.zeros((N+1+ng, N+ng, nbfaces))
            self.ucontra_averaged = np.zeros((N+1+ng, N+ng, nbfaces)) # used for departure point
            self.ucontra_old      = np.zeros((N+1+ng, N+ng, nbfaces)) # used for departure point

            self.upos, self.uneg = None, None

        elif pos == 'pv':
            self.ulon = np.zeros((N+ng, N+1+ng, nbfaces))
            self.vlat = np.zeros((N+ng, N+1+ng, nbfaces))
            self.ucontra = np.zeros((N+ng, N+1+ng, nbfaces))
            self.vcontra = np.zeros((N+ng, N+1+ng, nbfaces))
            self.vcontra_averaged = np.zeros((N+ng, N+1+ng, nbfaces)) # used for departure point
            self.vcontra_old      = np.zeros((N+ng, N+1+ng, nbfaces)) # used for departure point
            self.vpos, self.vneg = None, None

        elif pos == 'pc': # Velocity at pc
            self.ulon = np.zeros((N+ng, N+ng, nbfaces))
            self.vlat = np.zeros((N+ng, N+ng, nbfaces))
            self.ucontra = np.zeros((N+ng, N+ng, nbfaces))
            self.vcontra = np.zeros((N+ng, N+ng, nbfaces))
            self.vcontra_averaged = np.zeros((N+ng, N+ng, nbfaces)) # used for departure point
            self.vcontra_old      = np.zeros((N+ng, N+ng, nbfaces)) # used for departure point
            self.upos, self.uneg = None, None
            self.vpos, self.vneg = None, None
        else:
            print('ERROR in  velocity class: invalid position, ', pos)


from twilio.rest import Client
from prettytable import PrettyTable

account_sid = '' # account sid from twilio
auth_token = '' # account auth token from twilio
to_number = 'whatsapp:+44xxxxxxxxxx' # phone number

client = Client(account_sid, auth_token)

def make_message(parsed_weather):
    """takes a dict of weather data and processes into a message to send to whatsapp"""
    table = PrettyTable(['T'.center(6), '°C'.center(6), 'description', 'c%'.center(3), 'ws'.center(4)])
    for p in ['00', '03', '06', '09', '12', '15', '18', '21']:
        table.add_row(parsed_weather[p])
    # print(table)
    return table

def send_message(message):
    """uses twilio to send a whatsapp message to a phone number"""
    print(message)
    message = client.messages.create(
        body=message,
        from_='whatsapp:+14155238886',
        to=to_number
    )


def board_to_num(board):
    num = 0
    for row in range(9):
        for column in range(9):
            num = (num << 4) + board[row][column]
    return num

def num_to_board(num):
    board = [[0 for i in range(9)] for i in range(9)]
    for row in range(9):
        for column in range(9):
            pos = (8-row)*36 + (8-column)*4
            curr_val = (num >> pos) & 15
            board[row][column] = curr_val
    return board

class sudoku_board():
    def __init__(self, board):
        self.num_board = board_to_num(board)
        # sudoku board direpresentasikan menjadi suatu integer 324 bit
        self.solution = 0

    def show_board(self):
        board = num_to_board(self.num_board)
        str_show = ''
        for row in range(9):
            for column in range(9):
                str_show += str(board[row][column]) + ' '
            str_show += '\n'
        print(str_show)

    def val_index(self, row, column):
        # Memberikan nilai pada suatu board
        pos = (8-row)*36 + (8-column)*4
        return (self.num_board >> pos) & 15

    def change_block(self, row, column, num):
        # mengubah nilai pada suatu board
        pos = (8-row)*36 + (8-column)*4
        back = self.num_board & ((1 << (pos)) -1)
        front = self.num_board >> (pos+4)
        self.num_board = (((front << 4) + num) << pos) + back

    def is_valid_num(self, row, column, num):
        # check row
        for j in range(9):
            if num == self.val_index(row,j):
                return False
        # check column
        for i in range(9):
            if num == self.val_index(i, column):
                return False
        # check section
        index_i = (row // 3)*3
        index_j = (column // 3)*3
        for add_i in range(3):
            for add_j in range(3):
                if num == self.val_index(index_i+add_i,index_j+add_j):
                    return False
        return True

    def possible_num(self, row, column):
        # Mengecek angka yang mungkin untuk suatu kotak kosong
        if self.val_index(row, column) != 0:
            return []
        ans = (1 << 9) -1
        for j in range(9):
            if self.val_index(row, j) != 0:
                pos = 9-self.val_index(row, j)
                back = ans & ((1 << (pos)) -1)
                front = ans >> (pos+1)
                ans = (front << (pos+1)) + back
        for i in range(9):
            if self.val_index(i, column) != 0:
                pos = 9-self.val_index(i, column)
                back = ans & ((1 << (pos)) -1)
                front = ans >> (pos+1)
                ans = (front << (pos+1)) + back
        index_i = (row // 3)*3
        index_j = (column // 3)*3
        for add_i in range(3):
            for add_j in range(3):
                if self.val_index(index_i+add_i,index_j+add_j) != 0:
                    pos = 9-self.val_index(index_i+add_i,index_j+add_j)
                    back = ans & ((1 << (pos)) -1)
                    front = ans >> (pos+1)
                    ans = (front << (pos+1)) + back
        arr_ans = []
        for i in range(1,10):
            if ans & (1 << (9-i)):
                arr_ans.append(i)
        return arr_ans

    def solve_block(self, row, column):
        # recursion function
        # base case
        if row == 8 and column == 8:
            if self.val_index(8,8) != 0:
                self.show_board()
                print()
                self.solution = self.num_board
            else:
                self.change_block(8,8, self.possible_num(8,8)[0])
                self.show_board()
                print()
                self.solution = self.num_board
            return True
        if column == 8:
            next_block = (row+1, 0)
        else:
            next_block  = (row, column + 1)
        if self.val_index(row, column) != 0:
            return self.solve_block(next_block[0], next_block[1])

        else:
            if len(self.possible_num(row, column)) == 0:
                return False
            temp = self.num_board
            for num in self.possible_num(row, column):
                self.num_board = temp
                self.change_block(row, column, num)
                ans = self.solve_block(next_block[0], next_block[1])
                if ans == True:
                    return True

    def solve(self):
        print(self.solve_block(0,0))


'''

Not efficient way .. but gives the solution

'''

class Solution:
    def solve(self, words, s):
        if s ==  "":
            return True

        i = 0
        while  i <= len(s):
            if s[:i]  in words :
                if self.solve(words , s[i:]) :
                    print(f'back to the function with string {s}')
                    return True

            i += 1

        return False


import copy
from collections import defaultdict
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from enum import Enum
from typing import DefaultDict
from typing import Dict
from typing import List

from typing_extensions import TypedDict

from pager_duty_stats.logic.incident_types import extract_incident_type
from pager_duty_stats.logic.incident_types import ExtractionTechnique
from pager_duty_stats.logic.util.dates import step_through_dates

MISCELLANEOUS = 'miscellaneous'

class GroupingWindow(Enum):
    DAY = 'day'
    WEEK = 'week'

class AggregationType(Enum):
    SERVICE_NAME = 'service_name'
    TIME_OF_DAY = 'time_of_day'
    CUSTOM_INCIDENT_TYPE = 'custom_incident_type'

class AggregrateStats(TypedDict):
    total_pages: int
    aggregations: Dict[AggregationType, DefaultDict[str, int]]

class IncidentTime(Enum):
    WORK = 'work'
    SLEEP = 'sleep'
    LEISURE = 'leisure'

    # useful for making output human readable
    def __str__(self):
        return self.value

def get_local_datetime(iso_date: str):
    return datetime.strptime(iso_date, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc).astimezone(tz=None)

def get_fresh_aggregate_stats(aggregation_types: List[AggregationType]) -> AggregrateStats:
    return AggregrateStats(
        total_pages=0,
        aggregations={
            aggregation_type: defaultdict(int)
            for aggregation_type in aggregation_types
        }
    )

def is_week_day(time: datetime) -> bool:
    return time.weekday() < 5

def classify_incident_time(time: datetime) -> IncidentTime:
    if time.hour < 8:
        return IncidentTime.SLEEP

    if not is_week_day(time):
        return IncidentTime.LEISURE

    return IncidentTime.WORK if time.hour < 18 else IncidentTime.LEISURE

def extract_aggregation_value(
    incident: Dict,
    aggregation_type: AggregationType,
    incident_type_extraction_technique: ExtractionTechnique
) -> str:
    if aggregation_type == AggregationType.SERVICE_NAME:
        return incident['service']['summary']

    if aggregation_type == AggregationType.TIME_OF_DAY:
        return str(
            classify_incident_time(
                get_local_datetime(
                    incident['created_at']
                )
            )
        )

    if aggregation_type == AggregationType.CUSTOM_INCIDENT_TYPE:
        return extract_incident_type(
            incident,
            incident_type_extraction_technique
        )

    raise Exception('Aggregation group {} not supported!'.format(aggregation_type))  # pragma nocover

def get_stats_by_day(
    incidents: List[Dict],
    start_date: str,
    end_date: str,
    aggregation_types: List[AggregationType],
    max_incident_types: int,
    incident_type_extraction_technique: ExtractionTechnique
) -> Dict[str, AggregrateStats]:
    incidents_by_day: Dict[str, AggregrateStats] = {}

    for incident in incidents:
        create_date_time = get_local_datetime(incident['created_at'])
        create_date = str(create_date_time.date())
        if create_date not in incidents_by_day:
            incidents_by_day[create_date] = get_fresh_aggregate_stats(aggregation_types)

        incidents_by_day[create_date]['total_pages'] += 1

        for aggregation_type in aggregation_types:
            incidents_by_day[create_date]['aggregations'][aggregation_type][
                extract_aggregation_value(
                    incident,
                    aggregation_type,
                    incident_type_extraction_technique
                )
            ] += 1

    filled_out_stats = fill_out_empty_days(
        incidents_by_day,
        start_date,
        end_date,
        aggregation_types
    )

    if AggregationType.CUSTOM_INCIDENT_TYPE in aggregation_types:
        return clean_error_type_counts(
            filled_out_stats,
            max_incident_types
        )
    return filled_out_stats

def get_stats(
    incidents: List[Dict],
    start_date: str,
    end_date: str,
    grouping_window: GroupingWindow,
    aggregation_types: List[AggregationType],
    incident_type_extraction_technique: ExtractionTechnique,
    max_incident_types: int
):
    stats_by_day = get_stats_by_day(
        incidents,
        start_date,
        end_date,
        aggregation_types,
        max_incident_types,
        incident_type_extraction_technique
    )

    if grouping_window == GroupingWindow.DAY:
        return stats_by_day

    if grouping_window == GroupingWindow.WEEK:
        return convert_day_stats_to_week_stats(
            stats_by_day,
            end_date,
            aggregation_types
        )

    raise Exception('Grouping Window {} not recognized'.format(grouping_window))   # pragma: no cover

def fill_out_empty_days(
    stats: Dict[str, AggregrateStats],
    start_date: str,
    end_date: str,
    aggregation_types: List[AggregationType]
) -> Dict[str, AggregrateStats]:
    for date in step_through_dates(start_date, end_date):
        if date not in stats:
            stats[date] = get_fresh_aggregate_stats(aggregation_types)

    return stats

def get_earliest_date(dates: List[str]) -> str:
    earliest_date = str(datetime.now().date())
    for date in dates:
        if date < earliest_date:
            earliest_date = date
    return earliest_date

def find_next_monday(date: str) -> str:
    first_monday = datetime.strptime(date, '%Y-%m-%d')
    while first_monday.weekday() > 0:
        first_monday += timedelta(days=1)
    return str(first_monday.date())

def get_weekday_index(date: str) -> int:
    return datetime.strptime(date, '%Y-%m-%d').weekday()

def convert_day_stats_to_week_stats(
    stats: Dict[str, AggregrateStats],
    end_date: str,
    aggregation_types: List[AggregationType]
) -> Dict[str, AggregrateStats]:
    week_stats: Dict[str, AggregrateStats] = {}
    running_week_stats = get_fresh_aggregate_stats(aggregation_types)
    start_of_week = None

    start_date = find_next_monday(
        get_earliest_date(list(stats.keys()))
    )
    for date in step_through_dates(
        start_date,
        end_date
    ):
        if get_weekday_index(date) == 0:
            if start_of_week:
                week_stats[start_of_week] = running_week_stats

            running_week_stats = get_fresh_aggregate_stats(aggregation_types)
            start_of_week = date

        if date in stats:
            running_week_stats['total_pages'] += stats[date]['total_pages']

            for aggregation_type in aggregation_types:
                for name, count in stats[date]['aggregations'][aggregation_type].items():
                    running_week_stats['aggregations'][aggregation_type][name] += count

    if running_week_stats['total_pages'] > 0 and start_of_week:
        # We don't have a full week... include it anyways
        week_stats[start_of_week] = running_week_stats

    return week_stats

def clean_error_type_counts(
    stats: Dict[str, AggregrateStats],
    max_incident_types: int
) -> Dict[str, AggregrateStats]:
    # removes any errors that don't happen v often
    total_error_type_counts = {}
    for _, day_stats in stats.items():
        for error_type, error_type_count in day_stats['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE].items():
            if error_type not in total_error_type_counts:
                total_error_type_counts[error_type] = 0
            total_error_type_counts[error_type] += error_type_count

    total_error_type_counts_list = [(key, val) for key, val in total_error_type_counts.items()]
    total_error_type_counts_list_s = sorted(total_error_type_counts_list, key=lambda pair: -1 * pair[1])
    if len(total_error_type_counts_list_s) <= max_incident_types:
        # no need to hide anything
        return stats

    stats_to_keep = set(
        [incident_types for incident_types, _ in total_error_type_counts_list_s[:max_incident_types]]
    )

    new_stats = {}
    for day, _ in stats.items():
        new_stats[day] = copy.deepcopy(stats[day])
        for error_type, _ in stats[day]['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE].items():
            if error_type not in stats_to_keep:
                if MISCELLANEOUS not in new_stats[day]['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE]:
                    new_stats[day]['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE][MISCELLANEOUS] = 0

                new_stats[day]['aggregations'][
                    AggregationType.CUSTOM_INCIDENT_TYPE
                ][MISCELLANEOUS] += stats[day]['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE][error_type]

                del new_stats[day]['aggregations'][AggregationType.CUSTOM_INCIDENT_TYPE][error_type]

    return new_stats


import logging

from django.conf import settings

from matchlib.util import TokenMatch

from data.models import Comparison
import radar.config as config_loaders

logger = logging.getLogger("radar.matcher")

def safe_div(a, b):
    return a / b if b > 0 else 0.0

def top_marks(length, top):
    return '1' * top + '0' * (length - top)

def update_submission(submission_a, similarity, submission_b):
    """
    After matching a submission, update all flags and set new max similarity if the resulting similarity is highest so far.
    """
    submission_a.matched = True
    submission_a.matching_start_time = None
    submission_a.invalid = False
    if submission_a.max_similarity < similarity:
        submission_a.max_similarity = similarity
        submission_a.max_with = submission_b
    submission_a.save()

    # We don't need auto pause anymore because all matching tasks are started manually
    # if submission.max_similarity > settings.AUTO_PAUSE_MEAN:
    #     subs = submission.exercise.valid_matched_submissions
    #     if subs.count() > settings.AUTO_PAUSE_COUNT:
    #         avg = subs.aggregate(max_sim=Avg("max_similarity"))
    #         if avg["max_sim"] > settings.AUTO_PAUSE_MEAN:
    #             submission.exercise.paused = True
    #             submission.exercise.save()

def match_against_template(submission):
    """
    Match submission against the exercise template.
    """
    logger.debug("Match %s vs template", submission.student.key)
    # Template comparisons are defined as Comparison objects where the other (b) submission is null
    comparison = Comparison(submission_a=submission, submission_b=None, similarity=0.0, matches_json="[]")

    submission_tokens, template_tokens = submission.tokens, submission.exercise.template_tokens

    # Calculate amount of submission tokens that match the beginning of the exercise template
    l = 0
    while (l < len(submission_tokens) and l < len(template_tokens)
           and submission_tokens[l] == template_tokens[l]):
        l += 1
    template_head_match_count = l

    # Skip matching for the template head, we already checked the matches
    submission_marks = top_marks(len(submission_tokens), template_head_match_count)
    template_marks = top_marks(len(template_tokens), template_head_match_count)

    # Import similarity function and do comparison
    similarity_function = config_loaders.named_function(settings.MATCH_ALGORITHM)
    matches = similarity_function(
        submission_tokens,
        submission_marks,
        template_tokens,
        template_marks,
        submission.exercise.minimum_match_tokens
    )
    # Add the template head match
    if template_head_match_count > 0:
        matches.add_non_overlapping(TokenMatch(0, 0, template_head_match_count))

    comparison.similarity = safe_div(matches.token_count(), len(submission_tokens))
    comparison.matches_json = matches.json()

    return comparison


num=int(input());
mid=[];
for i in range(num-1):
    string=list(map(int,input().split()))
    mid.append(string);
fath=[[0,0] for i in range(num+1)];
for i in mid:
    fath[i[1]]=[i[0],i[2]];
count=0;
result=[];
for i in range(1,num+1):
    for j in range(i+1,num+1):
        if(i!=j):
            string="";
            string1="";
            string2="";
            string1count=[];
            string2count=[];
            k=i;
            l=j;
            while((fath[l][0]!=0)| (fath[k][0] != 0)):
                string1count.append(k);
                string1 = string1 + str(fath[k][1]);
                if(fath[k][0]!=0):
                    if(fath[k][0]==j):
                        string2="";
                        break;
                    if(fath[k][0] in string2count):
                        string2=string2[:string2count.index(fath[k][0])];
                        break;
                    k = fath[k][0];

                if(fath[l][0]!=0):
                    string2count.append(l)
                    string2 = string2 + str(fath[l][1]);
                    if (fath[l][0] == i):
                        string1 = "";
                        break;
                    if (fath[l][0] in string1count):
                        string1 = string1[:string1count.index(fath[l][0]) ];
                        break;
                    l = fath[l][0];
                if(fath[k][0]==fath[l][0]):
                    break;

            string=string1+string2[::-1];
            Rstring=string[::-1];
            if(string==Rstring):
                count+=1;
                result.append([i,j]);
print(count);


# -*- coding: utf-8 -*-

import re
import urllib
from urllib.request import urlopen
import urllib.parse
from tkinter import *

class Youdao:
    def __init__(self):
        # 有道翻译的释义所在的标签
        self.trans_tag_youdao = re.compile(r'\s?
.*?
\s?')

    # 爬虫需要的 url
    def getUrl(self):
        word = entry_word.get().replace(' ', '')
        if urllib.parse.quote(word) == word:
            search_url = "http://dict.youdao.com/w/" + urllib.parse.quote(word) + "/#keyfrom=dict2.top"
            return search_url

    # 爬取给定 url 的页面
    def getPage(self):
        search_url_youdao = self.getUrl()
        # 浏览器请求
        user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'
        headers = {'User-Agent': user_agent}
        req = urllib.request.Request(search_url_youdao, headers=headers)
        response = urllib.request.urlopen(req)
        # 将其他编码的字符串转换成 unicode 编码
        the_page = response.read().decode("utf-8")
        return the_page

    # 提取网页中所需要的信息
    def extractPage(self):
        paraphrase = ""
        # 获取页面
        page = self.getPage()

        # 获取有道的基本翻译
        trans_youdao = self.trans_tag_youdao
        paraphrase += "有道词典翻译: \n"
        paraphrase += "\n"
        # 获取标签之间的所有内容（包含释义）
        items = re.findall('
(.*?)
', page, re.S)

        for item in items:
            youdao_tmp = item
            trans_iterator_youdao = trans_youdao.finditer(youdao_tmp)
            for iterator in trans_iterator_youdao:
                tag_paraphrase = iterator.group()
                tag_youdao = re.compile('\s?<.*?>')
                paraphrase += tag_youdao.sub('', tag_paraphrase) + '\n'
        paraphrase += "\n"
        return paraphrase

    # 获得查询单词，调用 extracPage( ) 函数，获得释义，将释义传给窗口变量
    def getParaphrase(self):
        en_ch = entry_word.get().replace(' ', '')
        if en_ch == '':
            strs = "请输入您要查询的单词！"
            text_paraphrase.set(strs)
        elif en_ch.isalpha() or en_ch.isdigit():
            paraphrase = self.extractPage()
            if paraphrase == "" or paraphrase == "有道词典翻译: \n" + '\n' + '\n':
                strs = "没有发现该单词——%s的释义！" % en_ch
                text_paraphrase.set(strs)
            else:
                text_paraphrase.set(paraphrase)
        else:
            strs = "您的输入有误，请重新输入您要查询的单词！"
            text_paraphrase.set(strs)

if __name__ == '__main__':
    mydict = Youdao()

    # 绘制窗口，并分配小部件位置，同时设置各部件参数
    root = Tk()
    root.title('英汉词典')
    root.geometry('800x600')
    label_word = Label(root, text="查询:", font=("Helvetica", 16), width=8, height=3)
    entry_word = Entry(root, font=("Helvetica", 16), width=50)
    label_paraphrase = Label(root, text="释义:", font=("Helvetica", 16), width=8, height=3)
    text_paraphrase = StringVar()
    out_paraphrase = Label(root, textvariable=text_paraphrase, font=("Helvetica", 16), bg='white', width=50, height=17, wraplength=550, justify='left')
    label_word.grid(row=0, column=0)
    entry_word.grid(row=0, column=1)
    label_paraphrase.grid(row=1, column=0)
    out_paraphrase.grid(row=1, column=1)
    label_empty = Label(root, text="", font=("Helvetica", 16), width=1, height=1)
    label_empty.grid(row=2, column=0)
    btn_query = Button(root, text="查询", command=mydict.getParaphrase, font=("Helvetica", 16), width=10, height=1)
    btn_query.grid(row=3, column=1)
    root.mainloop()


#!/usr/bin/env python3
from random import randrange
from PIL import Image
import fileinput
import os

# the actual algorithm
r = randrange(255)
g = randrange(255)
b = randrange(255)

def Rigmarole(r, g, b):
    if (r > 200, g < 200, b < 200):
            g = g + 30
            b = b + 45
            r = r - 35

    if (r > 200):
        r = 200

    if (g > 235):
        g = 220

    if (b > 235):
        b = 220

    if (r + b + g >= 600):
        r = randrange(125) + 75
        g = randrange(150) + 100
        b = randrange(150) + 100
        if (r < 135, g < 135, b < 135):
            b = b + 100;
        if (r > 200, g < 200, b < 200):
            g = g + 30;
            b = b + 45;
            r = r - 35;

    if (r > b or r > g):
        r = r - 20
        b = b + 20
        g = g + 20

    if (r < 100):
        r = 100;

    if (g < 150):
        g = 150;

    if (b < 135):
        b = 135;

    return r, g, b

#r = Rigmarole(r,g,b)[0]
#g = Rigmarole(r,g,b)[1]
#b = Rigmarole(r,g,b)[2]

print(r)
print(g)
print(b)

currentFolder = os.path.dirname(__file__)
outputFolder = currentFolder + "/output"

if (os.path.exists(outputFolder) == False):
    os.mkdir(outputFolder)

output = outputFolder + "/rgb.png"
outputFile = Image.new("RGB", (64,64), (r,g,b))
outputFile.save(output, "PNG")