#------------------------------------------------------------------------------------------------
# KLayout forum No.1642
#
# Sub: Python Scripting for reading an Excel file and creating a polar array on Klayout Macro
# Posted by: dsenuka
#        on: September 16
#
# Author       : Kazzz-S
# First created: 2020-09-28 for KLayout ~= 0.26.8
# Last modified: 2024-07-11 for KLayout >= 0.29.4
#------------------------------------------------------------------------------------------------
# Usage:
#        (1) Store this script in ${HOME}/.klayout/pymacros/
#        (2) Start KLayout and open a New Layout
#        (3) Set the top cell name 'MetaLens'; database unit=MyDBU=0.001 [um]
#        (4) Start the Macro Development IDE tool
#        (5) Open this script and run
#------------------------------------------------------------------------------------------------
import sys
import os
import platform
import math
import pandas as pd
import pya

#-------------------------------------------------------------------------------------
# Global control parameters; modify and set these parameters as you like
#-------------------------------------------------------------------------------------
Debug          = True   # set False not to print debug messages
NumVertices    = None   # set an integer like 32 or 16 to fix the number of vertices
                        # of a polygon that approximates a circle
Convert2Static = False  # set True to convert PCells to static cells
MyDBU          = 0.001  # set an appropriate database unit in [um]
NumDigits      = 8      # set an appropriate number of digits below the decimal point
MyRoot         = os.environ["HOME"] + "/KLayout/Forum1642A/"
MyCSV          = MyRoot + "RealKL1642.csv"
OutBase        = MyRoot + "RealKL1642A"

#-------------------------------------------------------------------------------------
## Get KLayout version number
#
# @return (verstring, versionInt)-tuple
#-------------------------------------------------------------------------------------
def KLayoutVersionNumber():
  verstring    = pya.Application().instance().version() # like "KLayout 0.29.4"
  name, vernum = verstring.split(" ")
  vMajor       = "0"
  vMinor       = "0"
  vRev         = "0"
  try:
    vMajor, vMinor, vRev = vernum.split(".")
  except:
    try:
      vMajor, vMinor = vernum.split(".")  # like "KLayout 0.25"
    except:
      pass
  versionInt = 1000*int(vMajor) + 100*int(vMinor) + 1*int(vRev)

  return (verstring, versionInt)

#-------------------------------------------------------------------------------------
## Compute the number of vertices of a regular-N polygon approximating a circle
#
# @param[in] radius     radius in [um]
# @param[in] tol        tolerance in [um] (default=0.001[um])
# @param[in] integral4  the number should be an integral multiple of four (default=True)
#
# @return the number of vertices
#-------------------------------------------------------------------------------------
def ComputeNumberVerticies( radius, tol=0.001, integral4=True ):
  global NumVertices

  if not NumVertices == None:
    return NumVertices

  x = 1.0 - tol / radius
  y = math.acos(x)
  z = int(math.pi / y)
  if not integral4:
    numver = z
  else:
    res = z % 4
    if   res == 0:
      numver = z
    elif res == 1:
      numver = z + 3
    elif res == 2:
      numver = z + 2
    elif res == 3:
      numver = z + 1
    else:
      numver = 0 # dummy
  return numver

#-------------------------------------------------------------------------------------
# KLayout's major objects
#-------------------------------------------------------------------------------------
MainWindow  = pya.MainWindow.instance()
CurrentView = MainWindow.current_view()
Layout      = CurrentView.active_cellview().layout()
DesignFname = CurrentView.active_cellview().filename()
TopCellPtr  = Layout.top_cell()
TopCellName = TopCellPtr.name   # Python getter
CurrentDBU  = Layout.dbu        # Python getter; database unit
if not CurrentDBU == MyDBU:
  raise Exception( "! MyDBU=%f but CurrentDBU=%f" % (MyDBU, CurrentDBU) )
QuadrantCellPtr  = Layout.create_cell("quadrant")
QuadrantCellIdx  = QuadrantCellPtr.cell_index()
QuadrantCellName = QuadrantCellPtr.name

#---------------------------------------------------------------------------------------
## Construct the circle dictionary by reading the (modified) CSV file with four columns
#
#    #------------------------------------------------------------------------------------------------
#    # KLayout forum No.1642
#    #
#    # Sub: Python Scripting for reading an Excel file and creating a polar array on Klayout Macro
#    # Posted by: dsenuka
#    #        on: September 16
#    #
#    # Source       : all_data_table_wavelength_4um_lens_radius_2000um_f#_2_with_min_and_max_angles.xlsx
#    # Remade by    : Kazzz-S
#    # Last modified: 2020-09-27
#    #------------------------------------------------------------------------------------------------
#    pillar number,x coordinate [um],y coordinate [um],orbit radius [um],circle diameter [um],rotation angle [rad],number of pillars to built (to make a quarter),From angle [rad],Up to angle [rad],Up to angle [rad]
#    1,0.75000000 ,0.75000000 ,1.06066017 ,1.09998989 ,1.57079633 ,1 ,0.78539816 ,7.854E-01,0.78539816
#    2,2.25000000 ,0.93198052 ,2.43538245 ,1.09990900 ,0.78539816 ,2 ,0.39269908 ,1.178E+00,1.17809725
#    :
#    :
#    1333,"1,998.75000000 ",0.75003089 ,"1,998.75014072 ",0.62390673 ,0.00075050 ,"2,093 ",0.00037525 ,1.570E+00,1.57042108
#    1334,"2,000.25000000 ",0.75023532 ,"2,000.25014070 ",1.06437124 ,0.00075014 ,"2,094 ",0.00037507 ,1.570E+00,1.57042126
#    1335,"2,001.75000000 ",0.75008151 ,"2,001.75014053 ",0.97396148 ,0.00074943 ,"2,096 ",0.00037471 ,1.570E+00,1.57042161
#
#---------------------------------------------------------------------------------------
def ConstructCircleDictionary():
  global CircleDic    # the circle dictionary

  CircleDic   = dict()
  df          = pd.read_csv( MyCSV, comment='#' )
  numLeafCell = len(df)
  for idx in range(0, numLeafCell):
    cirID    = int  ( df.iloc[idx,0] )
    startX   = float( df.iloc[idx,1] )
    startY   = float( df.iloc[idx,2] )
    orbitRad = float( df.iloc[idx,3] )
    cirDia   = float( df.iloc[idx,4] )
    rotAngle = float( df.iloc[idx,5] )
    numInst  = int  ( df.iloc[idx,6] )
    CircleDic[cirID] = (startX, startY, orbitRad, cirDia, rotAngle, numInst)

  if Debug:
    print( "### Contents of CircleDic ###" )
    for cirID in sorted(CircleDic.keys()):
      print( cirID, CircleDic[cirID] )
    print( "" )

#-------------------------------------------------------------------------------
## Create different circles as PCells and populate the cell dictionary
#-------------------------------------------------------------------------------
def CreateLeafPCells():
  global CellDic    # the cell dictionary

  CellDic = dict()

  #-----------------------------------------------------------------------
  # [1] Create each PCell as "CIRCLE" (not "ELLIPSE")
  #-----------------------------------------------------------------------
  for cirID in sorted(CircleDic.keys()):
    #---------------------------------------------------------------------
    # (A) Populate a dictionary to pass PCell parameters
    #---------------------------------------------------------------------
    (startX, startY, orbitRad, cirDia, rotAngle, numInst) = CircleDic[cirID]

    cellName = "C%04d" % cirID # max. is 1335
    numVer   = ComputeNumberVerticies( round( cirDia / 2.0, NumDigits ), tol=MyDBU )

    paraL = "layer"         # Layer and Datatype
    paraA = "actual_radius" # hidden parameter that actually changes the radius
    paraN = "npoints"       # number of vertices of a polygon approximating the circle

    params = dict()
    params[paraL] = pya.LayerInfo( 1, 0 ) # use a single layer for simplicity
    params[paraA] = round( cirDia / 2.0, NumDigits )
    params[paraN] = numVer

    #---------------------------------------------------------------------
    # (B) Create a PCell and convert it to a static cell on demand
    #---------------------------------------------------------------------
    sbcell1 = Layout.create_cell( "CIRCLE", "Basic", params ).cell_index()
    if Convert2Static:
      sbcell2 = Layout.convert_cell_to_static( sbcell1 )
      Layout.rename_cell( sbcell2, cellName )
      Layout.delete_cell( sbcell1 )
      CellDic[cirID] = sbcell2
      if Debug:
        print( "Leaf static cell <%s> has Cell-ID <%d>" % (cellName, sbcell2) )
    else:
      Layout.rename_cell( sbcell1, cellName )
      CellDic[cirID] = sbcell1
      if Debug:
        print( "Leaf PCell <%s> has Cell-ID <%d>" % (cellName, sbcell1) )

  #-----------------------------------------------------------------------
  # [2] Take care of the newly added layers
  #-----------------------------------------------------------------------
  CurrentView.add_missing_layers()  # add missing layers
  CurrentView.zoom_fit()            # zoom to fit the entire hierarchy

#-------------------------------------------------------------------------------
## Create the circle array in a quarter (quadrant-I)
#-------------------------------------------------------------------------------
def CreateCircleArray():
  global TotalInstances   # total number of instances inserted

  TotalInstances = 0
  for cirID in sorted(CircleDic.keys()):

    #-----------------------------------------------------
    # [1] Look up the dictionaries
    #-----------------------------------------------------
    (startX, startY, orbitRad, cirDia, rotAngle, numInst) = CircleDic[cirID]
    startAngle = math.atan2( startY, startX )
    leafCell   = CellDic[cirID]

    #-----------------------------------------------------
    # [2] Place the leaf cell one by one
    #-----------------------------------------------------
    for idx in range(0, numInst):
      #-----------------------------------------------------
      # (A) Set the location on the orbit
      #-----------------------------------------------------
      if idx == 0:
        x = round( startX, NumDigits )
        y = round( startY, NumDigits )
      else:
        theta = startAngle + idx * rotAngle
        x     = round( orbitRad * math.cos(theta), NumDigits )
        y     = round( orbitRad * math.sin(theta), NumDigits )

      #-----------------------------------------------------
      # (B) SREF the PCell
      #-----------------------------------------------------
      dispVec = pya.DTrans( x/MyDBU, y/MyDBU )  # displace cell by (x, y) [um]
      cellIns = pya.CellInstArray()             # default cell instance
      cellIns.cell_index = leafCell             # 'set' the cell to refer (what to refer)
      cellIns.trans = dispVec                   # 'set' the displacement vector (how to refer)
      QuadrantCellPtr.insert( cellIns )         # insert the instance to [QuadrantCellName]
      TotalInstances += 1

#-------------------------------------------------------------------------------
## Make the full-circle array
#-------------------------------------------------------------------------------
def MakeFullCircle():
  #-------------------------------------------------------------------------
  # [1] For quadrant-[I, II, III, IV], SREF QuadrantCellPtr into TopCellPtr
  #-------------------------------------------------------------------------
  listTrans = list()
  listTrans.append( pya.Trans().R0 )
  listTrans.append( pya.Trans().R90 )
  listTrans.append( pya.Trans().R180 )
  listTrans.append( pya.Trans().R270 )

  for trans in listTrans:
    cellIns = pya.CellInstArray()
    cellIns.cell_index = QuadrantCellIdx # child cell
    cellIns.trans = trans
    TopCellPtr.insert(cellIns)

#-------------------------------------------------------------------------------
## Count cell references recursively
#
# @param[in] cell         root cell
# @param[in] dicRefCount  reference count dictionary
#
# @return dicRefCount
#-------------------------------------------------------------------------------
def CountCellReferences( cell, dicRefCount=None ):
  if dicRefCount == None:
    dicRefCount = dict()

  for inst in cell.each_inst():
    child_cell = inst.cell
    if child_cell.name in dicRefCount:
      dicRefCount[child_cell.name] += 1
    else:
      dicRefCount[child_cell.name] = 1

    CountCellReferences( child_cell, dicRefCount )

  return dicRefCount

#-------------------------------------------------------------------------------
## Generate the output file
#-------------------------------------------------------------------------------
def GenerateOutput():
  sop = pya.SaveLayoutOptions()   # default save option
  out =  OutBase + ".oas"
  Layout.write( out, sop )   # *.oas makes KLayout infer the output format
  print( "" )
  print( "### Generated", out )

#===============================================================================
# Main()
#===============================================================================

#-------------------------------------------------------------
# [0] Get machine environment and start the timer
#-------------------------------------------------------------
verstring, versionInt = KLayoutVersionNumber()
if versionInt < 2904:
  print( " Error: expect KLayout version >= 0.29.4 but actual version == %s" % verstring, file=sys.stderr )
  quit()

(System, Node, Release, Version, Machine, Processor) = platform.uname()
myTimer = pya.Timer()
myTimer.start()

#-------------------------------------------------------------
# [1] Construct the cell hierarchy
#-------------------------------------------------------------
ConstructCircleDictionary()
CreateLeafPCells()
CreateCircleArray()
MakeFullCircle()
dicRefCount = CountCellReferences(TopCellPtr)
totalRefCount = 0
if Debug:
  print("")
  print("----- Reference Count -----")
for cell_name, count in dicRefCount.items():
  totalRefCount += count
  if Debug:
    print(f"  Cell {cell_name} is referenced {count} times.")

#-------------------------------------------------------------
# [2] Change the hierarchy level
#-------------------------------------------------------------
CurrentView.max_hier_levels = 3
CurrentView.zoom_fit()

#-------------------------------------------------------------
# [3] Generate the output file
#-------------------------------------------------------------
GenerateOutput()

#-------------------------------------------------------------
# [4] Print timing info
#-------------------------------------------------------------
myTimer.stop()
usingPCell = { True:"No", False:"Yes" } # key=Convert2Static
print( "" )
print( "### <%s> processed <%s> ###" % ( os.path.basename(__file__), os.path.basename(MyCSV) ) )
print( "    (a) KLayout version: %s" % pya.Application().instance().version() )
print( "    (b) Machine info: %s, %s, %s" % (System, Node, Machine) )
print( "    (c) Using PCell?: %s" % usingPCell[ Convert2Static ] )
print( "    (d) Total number of instances inserted to 'quadrant': %s" % "{:,}".format(TotalInstances) )
print( "    (e) Total number of instances under '%s': %s" % (TopCellName, "{:,}".format(totalRefCount)) )
print( "    (f) Verification: 4 x (d) + 4 - (e) = %d" % (4*TotalInstances + 4 - totalRefCount) )
print( "    (g) Timing info: Sys=%.3f[sec]  User=%.3f[sec]  Wall=%.3f[sec]" % ( myTimer.sys(), myTimer.user(), myTimer.wall() ) )
print( "" )

#-------------------------------------------------------------
# [5] Manual operations can follow...
#-------------------------------------------------------------
# You can
#     (A) change the layer properties
#     (B) convert a PCell to a static cell, if not yet done
#     (C) flat the cell hierarchy
#     (D) change the layers
#     (E) and many more...
#
# Enjoy PYA scripting!
#-------------------------------------------------------------

#---------------
# End of file
#---------------
