#Copyright 2019 Russell Carroll
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.

from math import *
from numpy import *
import os

class Qucs2Gerber():

  # ----------------------------------------
  # General Class functions
  # ----------------------------------------

  def __init__(self):
    self.log_fn   = "qucs2grb_log.txt"
    self.out_fn   = "qucs_out.gbr"
    self.num_int  = 2
    self.num_decimals = 5
    self.unit_scale = 1.0 # Inches
    self.units = "in"
    self.verbose  = False
    self.out_fh   = None
    self.netlist_data = None
    self.log_print("-- QUCS to Gerber File Conversion Log --\n",'w')

  def __del__(self):
    self.kill()

  def kill(self):
    if self.out_fh:
      self.out_fh.close()

  def log_print(self,text,mode='a'):
    try:
      fh = open(self.log_fn,mode)
      fh.write(str(text) + "\n")
      fh.close()
    except:
      print("Error: Could not write to log file! " + self.log_fn)
      self.verbose = True

  # Print to screen and log
  def fprint(self,text,force=False):
    if self.verbose or force:
      print(str(text))
    self.log_print(text)

  # ----------------------------------------
  # Functions for Gerber File Generation
  # ----------------------------------------

  def SetDecimals(self,decimals,leading=2):
    try:
      self.num_decimals = int(decimals)
      self.num_int = int(leading)
    except:
      self.fprint("Error: Could not read number of decimals!")

  def SetUnits(self,units):
    if "in" in units:
      unit_scale = 1.0  # scale everything to inches
    elif "mm" in units:
      unit_scale = 0.0393701
    else:
      self.fprint("Error: Unknown units! " + units,force=True)
      return False
    self.units = units
    self.unit_scale = unit_scale

  def OpenOutpuFile(self,fn="output.grb"):
    self.out_fn = fn
    try:
      self.out_fh = open(self.out_fn,'w')
      return True
    except:
      self.fprint("Error: Could not output file! " + self.out_fn,force=True)
      self.verbose = True
      return False

  def out_write(self,text,append_str='\n'):
    try:
      self.out_fh.write(str(text) + append_str)
    except:
      self.fprint("Error: Could not write to output file! " + self.out_fn,force=True)
      self.verbose = True

  def GenerateHeader(self):
    header = [
      "G04 Generated using qucs2gerber.py*",
      "G04 Input file: {}*".format(self.netlist_fn),
      "%MO{}*%".format(self.units.upper()),             # Use relevant units
      "%LNTOP*%",                                       # Layer name TOP
      # Format specification, Leading zeros omitted, absolute coordinates, integer places, decimal places
      "%FSLAX{}{}Y{}{}*%".format(self.num_int,self.num_decimals,self.num_int,self.num_decimals),
      "%ADD11C,0.0001*%"    # Define aperature, D11 is a circle with diameter of 0.0001 inch
    ]
    for l in header:
      self.out_write(l)

  def Finish(self):
    self.out_write("M02*",append_str='')

  def get_int(self,f):
    return int(round(f*10**self.num_decimals))

  # Draw rectangle with left edge at the origin
  def DrawRectangleLEO(self,width=1.0,length=1.0,x0=0.0,y0=0.0,rot=0.0):
    rot_rad = rot*pi/180.0
    R = [[cos(rot_rad),sin(rot_rad)],[-sin(rot_rad),cos(rot_rad)]]  # rotation matrix
    # initial coordinates
    a = [0, -width/2.0]
    b = [0, +width/2.0]
    c = [length, +width/2.0]
    d = [length, -width/2.0]
    # rotated coordinates
    at = dot(a,R)
    bt = dot(b,R)
    ct = dot(c,R)
    dt = dot(d,R)
    commands = [
      "G54D11*",    # Select aperature D11
      "G36*",       # Enable region mode
      "G01*",
      "X{}Y{}D02*".format(self.get_int(at[0]+x0),self.get_int(at[1]+y0)),
      "X{}Y{}D01*".format(self.get_int(bt[0]+x0),self.get_int(bt[1]+y0)),
      "X{}Y{}D01*".format(self.get_int(ct[0]+x0),self.get_int(ct[1]+y0)),
      "X{}Y{}D01*".format(self.get_int(dt[0]+x0),self.get_int(dt[1]+y0)),
      "X{}Y{}D01*".format(self.get_int(at[0]+x0),self.get_int(at[1]+y0)),
      "G37*"        # Disable region mode
    ]
    for cmd in commands:
      self.out_write(cmd)

  # Draw polygon from list of corners
  def DrawPolygon(self,corners,x0=0.0,y0=0.0,rot=0.0):
    rot_rad = -rot*pi/180.0
    R = [[cos(rot_rad),sin(rot_rad)],[-sin(rot_rad),cos(rot_rad)]]  # rotation matrix
    # Generate transformed coordinates
    offset = array([x0,y0])
    ct = []
    for c in corners:
      ct.append(dot(R,c) + offset)
    ct.append(ct[0])
    # write the output
    self.out_write("G54D11*")
    self.out_write("G36*")
    self.out_write("G01*")  # Linear interpolation
    self.out_write("X{}Y{}D02*".format(self.get_int(ct[0][0]),self.get_int(ct[0][1])))
    for c in ct[1:]:
      cmd = "X{}Y{}D01*".format(self.get_int(c[0]),self.get_int(c[1]))
      self.out_write(cmd)
    self.out_write("G37*")

  def DrawMRSTUB(self,ri,ro,alpha,x0,y0,phi):
    # Calculate the paramters
    W = ri
    alpha_rad = alpha*pi/180.0
    l = W/(2*tan(alpha_rad/2))
    # Generate the relative points
    A = [0,W/2.0]
    B = [ro*cos(alpha_rad/2)-l,ro*sin(alpha_rad/2)]
    C = [ro*cos(alpha_rad/2)-l,-ro*sin(alpha_rad/2)]
    D = [0,-W/2.0]
    offset = array([-l,0])
    corners = [A,B,C,D,offset]
    # Rotate into the real coordinates
    rot_rad = -phi*pi/180.0
    R = array([[cos(rot_rad),sin(rot_rad)],[-sin(rot_rad),cos(rot_rad)]])  # rotation matrix
    ct = dot(R,array(corners).transpose()).transpose()
    for i in range(len(ct)):
      ct[i][0] = ct[i][0] + x0
      ct[i][1] = ct[i][1] + y0
    ct[4] = ct[4] - ct[1] # This makes the arc come out correctly in the gerber file
    # Initialize
    # write the output
    self.out_write("G54D11*")
    self.out_write("G36*")
    self.out_write("X{}Y{}D02*".format(self.get_int(ct[0][0]),self.get_int(ct[0][1])))
    self.out_write("X{}Y{}D01*".format(self.get_int(ct[1][0]),self.get_int(ct[1][1])))
    self.out_write("G75*")  # Multiquadrant
    self.out_write("G02*")  # Clockwise circular interpolation
    # Do the rotation
    self.out_write("X{}Y{}I{}J{}D01*".format(self.get_int(ct[2][0]),self.get_int(ct[2][1]),
      self.get_int(ct[4][0]),self.get_int(ct[4][1])))
    self.out_write("G01*")  # Linear interpolation
    self.out_write("X{}Y{}D01*".format(self.get_int(ct[2][0]),self.get_int(ct[2][1])))
    self.out_write("X{}Y{}D01*".format(self.get_int(ct[3][0]),self.get_int(ct[3][1])))
    self.out_write("X{}Y{}D01*".format(self.get_int(ct[0][0]),self.get_int(ct[0][1])))
    self.out_write("G37*")

  # ----------------------------------------
  # Functions for QUCS Netlist Parsing
  # ----------------------------------------

  def ReadNetlist(self,netlist_fn):
    self.netlist_fn = netlist_fn
    try:
      fh = open(netlist_fn,'r')
      self.netlist_data = fh.read()
      fh.close()
    except:
      self.fprint("Error: Could not read netlist file: " + netlist_fn,force=True)

  def GetParameter(self,line,param):
    try:
      param_list = line.split(" ")
      for a in param_list:
        if (param + "=") in a:
          return a.replace(param + "=","").replace("'","").replace('"','')
    except:
      self.fprint("Error: Could not find paramter: " + str(param),force=True)
      return False

  def RemoveSpaces(self,line):
    return line.replace(" mm","mm").replace(" mil","mil")

  def GetLength(self,length_str):
    if "mm" in length_str:
      return float(length_str.replace("mm",""))*0.0393701/self.unit_scale
    elif "mil" in length_str:
      return float(length_str.replace("mil",""))*0.001/self.unit_scale
    else:
      try:
        return float(length_str)
      except:
        self.fprint("Error: Unknown length unit! {}".format(length_str),force=True)

  def ParseNetlist(self):
    net_list = []
    elements = []
    lines = self.netlist_data.split("\n")
    for l in lines:
      l = self.RemoveSpaces(l)
      if "MLIN" in l:
        # MLIN:MLIN1 _net0 _net2 Subst="Subst1" W="1 mm" L="10 mm" Model="Hammerstad" DispModel="Kirschning" Temp="26.85"
        self.fprint("Found element: " + l)
        # Get parameters
        W = self.GetLength(self.GetParameter(l,"W"))
        L = self.GetLength(self.GetParameter(l,"L"))
        # Get nodes
        l = l.replace("MLIN:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        elements.append(["MLIN",l[0],l[1],l[2],W,L])
      elif "MTEE" in l:
        # MTEE:MS3 _net3 _net4 _net5 Subst="Subst1" W1="1 mm" W2="1 mm" W3="2 mm" MSModel="Hammerstad" MSDispModel="Kirschning" Temp="26.85"
        self.fprint("Found element: " + l)
        # Get parameters
        W1 = self.GetLength(self.GetParameter(l,"W1"))
        W2 = self.GetLength(self.GetParameter(l,"W2"))
        W3 = self.GetLength(self.GetParameter(l,"W3"))
        # Get nodes
        l = l.replace("MTEE:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        if l[3] not in net_list:
          net_list.append(l[3])
        elements.append(["MTEE",l[0],l[1],l[2],l[3],W1,W2,W3])
      elif "MCORN" in l:
        # MCORN:MS4 _net6 _net7 Subst="Subst1" W="1 mm"
        self.fprint("Found element: " + l)
        # Get parameters
        W = self.GetLength(self.GetParameter(l,"W"))
        # Get nodes
        l = l.replace("MCORN:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        elements.append(["MCORN",l[0],l[1],l[2],W])
      elif "MTAPER" in l:
        pass
      elif "MMBEND" in l:
        # MMBEND:MS5 _net8 _net9 Subst="Subst1" W="1 mm"
        self.fprint("Found element: " + l)
        # Get parameters
        W = self.GetLength(self.GetParameter(l,"W"))
        # Get nodes
        l = l.replace("MMBEND:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        elements.append(["MMBEND",l[0],l[1],l[2],W])
      elif "MSTEP" in l:
        # MSTEP:MS6 _net10 _net11 Subst="Subst1" W1="2 mm" W2="1 mm" MSModel="Hammerstad" MSDispModel="Kirschning"
        self.fprint("Found element: " + l)
        # Get parameters
        W1 = self.GetLength(self.GetParameter(l,"W1"))
        W2 = self.GetLength(self.GetParameter(l,"W2"))
        # Get nodes
        l = l.replace("MSTEP:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        elements.append(["MSTEP",l[0],l[1],l[2],W1,W2])
      elif "MSLIT" in l:
        pass
      elif "MGAP" in l:
        # MGAP:MS9 _net16 _net17 Subst="Subst1" W1="1 mm" W2="1 mm" S="1 mm" MSModel="Hammerstad" MSDispModel="Kirschning"
        self.fprint("Found element: " + l)
        # Get parameters
        W1 = self.GetLength(self.GetParameter(l,"W1"))
        W2 = self.GetLength(self.GetParameter(l,"W2"))
        S  = self.GetLength(self.GetParameter(l,"S"))
        # Get nodes
        l = l.replace("MGAP:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        elements.append(["MGAP",l[0],l[1],l[2],W1,W2,S])
      elif "MCURVE" in l:
        pass
      elif "MCURVE2" in l:
        pass
      elif "MRSTUB" in l:
        # MRSTUB:MS10 _net18 Subst="Subst1" ri="1 mm" ro="10 mm" alpha="90"
        self.fprint("Found element: " + l)
        # Get parameters
        ri = self.GetLength(self.GetParameter(l,"ri"))
        ro = self.GetLength(self.GetParameter(l,"ro"))
        a = self.GetLength(self.GetParameter(l,"alpha"))
        # Get nodes
        l = l.replace("MRSTUB:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        elements.append(["MRSTUB",l[0],l[1],ri,ro,a])
      elif "MBSTUB" in l:
        pass
      elif "MCFIL" in l:
        pass
      elif "MCOUPLED" in l:
        # MCOUPLED:MS5 _net8 _net9 _net7 _net10 Subst="Subst1" W="1 mm" L="10 mm" S="1 mm" Model="Kirschning" DispModel="Kirschning" Temp="26.85"
        self.fprint("Found element: " + l)
        # Get parameters
        W = self.GetLength(self.GetParameter(l,"W"))
        L = self.GetLength(self.GetParameter(l,"L"))
        S  = self.GetLength(self.GetParameter(l,"S"))
        # Get nodes
        l = l.replace("MCOUPLED:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        if l[3] not in net_list:
          net_list.append(l[3])
        if l[4] not in net_list:
          net_list.append(l[4])
        elements.append(["MCOUPLED",l[0],l[1],l[2],l[3],l[4],W,L,S])
        pass
      elif "MSABND" in l:
        pass
      elif "MSOBND" in l:
        pass
      elif "MCROSS" in l:
        # MCROSS:MS7 _net12 _net13 _net14 _net15 Subst="Subst1" W1="1 mm" W2="2 mm" W3="1 mm" W4="2 mm" MSModel="Hammerstad" MSDispModel="Kirschning"
        self.fprint("Found element: " + l)
        # Get parameters
        W1 = self.GetLength(self.GetParameter(l,"W1"))
        W2 = self.GetLength(self.GetParameter(l,"W2"))
        W3 = self.GetLength(self.GetParameter(l,"W3"))
        W4 = self.GetLength(self.GetParameter(l,"W4"))
        # Get nodes
        l = l.replace("MCROSS:","").strip().split(" ")
        if l[1] not in net_list:
          net_list.append(l[1])
        if l[2] not in net_list:
          net_list.append(l[2])
        if l[3] not in net_list:
          net_list.append(l[3])
        if l[4] not in net_list:
          net_list.append(l[4])
        elements.append(["MCROSS",l[0],l[1],l[2],l[3],l[4],W1,W2,W3,W4])
      elif "MCROSO" in l:
        pass
      elif "MSOP" in l:
        pass
      elif "VIAGND" in l:
        pass
      elif "VIA2" in l:
        pass

    self.net_list = net_list
    self.elements = elements
    self.fprint("Net List:")
    self.fprint(self.net_list)
    self.fprint("Elements:")
    self.fprint(self.elements)

    # Check the elements and netlist
    for net in self.net_list:
      used_cnt = 0
      used_list = []
      for e in elements:
        if net in e:
          used_cnt = used_cnt + 1
          used_list.append(e[1])
      if used_cnt > 2:
        self.fprint("Error: More than 2 components on a net! {} connected to {}".format(net,used_list),force=True)

  def GetNetIndex(self,net,element):
    if net in element:
      return element.index(net) - 2 # drop first two elements, reference to vector index
    else:
      return False

  def GetVectors(self,element):
    v = []  # gives [[x,y,phi],...]
    v.append([0,0,180]) # relative to first node
    l = element[0]
    if "MLIN" in l:
      L = element[5]
      v.append([L,0,0])  # Length in x direction
    elif "MTEE" in l:
      W1 = element[5]
      W2 = element[6]
      W3 = element[7]
      v.append([W3,0,0])
      v.append([W3/2,-max(W1,W2)/2.0,-90])
    elif "MCORN" in l:
      W = element[4]
      v.append([W/2.0,-W/2.0,-90])  # Length in x direction
    elif "MTAPER" in l:
      pass
    elif "MMBEND" in l:
      # elements.append(["MMBEND",l[0],l[1],l[2],W])
      W = element[4]
      v.append([W/2.0,-W/2.0,-90])  # Length in x direction
    elif "MSTEP" in l:
      # elements.append(["MSTEP",l[0],l[1],l[2],W1,W2])
      v.append([0,0,0])
    elif "MSLIT" in l:
      pass
    elif "MGAP" in l:
      # elements.append(["MGAP",l[0],l[1],l[2],W1,W2,S])
      S = element[6]
      v.append([S,0,0])  # Length in x direction
      pass
    elif "MCURVE" in l:
      pass
    elif "MCURVE" in l:
      pass
    elif "MCURVE2" in l:
      pass
    elif "MRSTUB" in l:
      pass
    elif "MBSTUB" in l:
      pass
    elif "MCFIL" in l:
      pass
    elif "MCLIN" in l:
      pass
    elif "MCOUPLED" in l:
      # elements.append(["MCOUPLED",l[0],l[1],l[2],l[3],l[4],W,L,S])
      W = element[6]
      L = element[7]
      S = element[8]
      v.append([0,-(W+S),180])
      v.append([L,-(W+S),0])
      v.append([L,0,0])
    elif "MSABND" in l:
      pass
    elif "MSOBND" in l:
      pass
    elif "MCROSS" in l:
      # elements.append(["MCROSS",l[0],l[1],l[2],l[3],l[4],W1,W2,W3,W4])
      W1 = element[6]
      W2 = element[7]
      W3 = element[8]
      W4 = element[9]
      x = max(W4,W2)
      y = max(W1,W3)
      v.append([x/2.0,y/2.0,90])
      v.append([x,0,0])
      v.append([x/2.0,-y/2.0,-90])
    elif "MCROSO" in l:
      pass
    elif "MSOP" in l:
      pass
    elif "VIAGND" in l:
      pass
    elif "VIA2" in l:
      pass
    return array(v)

  def InSlaveList(self,net="",elem=""):
    for s in self.slaves:
      if (net == s[0]) and (elem == s[1]):
        return True
    return False

  # This is the crazy function that actually decides how to route the microstrip lines by finding the coordinates
  def GetNextCoordinate(self):
    if len(self.coordinates) < 1:
      self.coordinates.append([self.elements[0][2],array([0,0,0])])  # Start with first element at the origin
      self.slaves.append([self.elements[0][2],self.elements[0][1]])
      return True
    # Look from the begining
    for c in self.coordinates:
      # Check each element
      for e in self.elements:
        v = self.GetVectors(e)
        L = len(v)
        # Look for new cordinates on this element
        if c[0] in e:
          for l in range(L):
            hit = False
            for s in self.coordinates:
              hit = hit or (e[l+2] == s[0])
            if not hit:
              # Check if the element is a slave of the coordinate
              slave = True
              for s in self.slaves:
                if (c[0] == s[0]) or (e[1] == s[1]):
                  slave = False
              if slave:
                self.slaves.append([c[0],e[1]])
              # New coordinate found. Add to list
              ind0 = self.GetNetIndex(c[0],e)
              va = v[l][0:2]
              vb = v[ind0][0:2]
              alpha = v[l][2]
              beta  = v[ind0][2]
              reverse_direction = False
              if self.InSlaveList(c[0],e[1]):
                self.fprint("{} is master of {}".format(e[1],e[l+2]))
                gamma = c[1][2]
              else:
                # Special slave case
                reverse_direction = True
                self.fprint("{} is slave of {}".format(e[1],e[l+2]))
                self.slaves.append([e[l+2],e[1]])
                gamma = c[1][2] -180
              theta =  180 - beta + gamma
              rot_rad = theta*pi/180.0
              R = [[cos(rot_rad),sin(rot_rad)],[-sin(rot_rad),cos(rot_rad)]]  # rotation matrix
              # rotated coordinates
              vt = dot(va-vb,R)
              v_new = array([vt[0],vt[1],0])
              new_c = v_new + c[1]
              if reverse_direction:
                phi = alpha + gamma - beta
              else:
                phi = alpha + gamma + 180 - beta
              while phi > 180.0:
                phi = phi - 360.0
              while phi < -180.0:
                phi = phi + 360.0

              new_c[2] = phi
              cor = [e[l+2],new_c]
              self.fprint("New coordinate: {} gives {}".format(e,cor))
              self.coordinates.append(cor)
              return True
    return False  # No more connections can be found

  def GetElement(self,refdes):
    for e in self.elements:
      if refdes == e[1]:
        return e
    self.fprintf("Error: Could not find element: {}".format(refdes),force=True)
    exit()

  def GetCoordinate(self,net):
    for c in self.coordinates:
      if net == c[0]:
        return c
    self.fprintf("Error: Could not find coordinate: {}".format(net),force=True)
    return False

  def WriteElement(self,element,x,y,phi):
    v = self.GetVectors(element)
    rot_rad = -phi*pi/180.0
    R = [[cos(rot_rad),sin(rot_rad)],[-sin(rot_rad),cos(rot_rad)]]  # rotation matrix
    l = element[0]
    if "MLIN" in l:
      W = element[4]
      L = element[5]
      self.DrawRectangleLEO(W,L,x,y,phi)
    elif "MTEE" in l:
      W1 = element[5]
      W2 = element[6]
      W3 = element[7]
      v21 = dot(R,v[1][0:2])
      v31 = dot(R,v[2][0:2])
      #self.fprint("MTEE {}: phi = {}, v = {}, v21 = {}, v31 {}".format(element[1],phi,v,v21,v31))
      self.DrawRectangleLEO(W1,W3/2.0,x,y,phi)  # Node 1
      self.DrawRectangleLEO(W2,W3/2.0,x+v21[0],y+v21[1],phi+180)  # Node 2
      self.DrawRectangleLEO(W3,max(W1,W2)/2.0,x+v31[0],y+v31[1],phi+90) # Node 3
    elif "MCORN" in l:
      W = element[4]
      self.DrawRectangleLEO(W,W,x,y,phi)
    elif "MTAPER" in l:
      pass
    elif "MMBEND" in l:
      # elements.append(["MMBEND",l[0],l[1],l[2],W])
      W = element[4]
      corners = [[0,W/2.0],[W,-W/2.0],[0,-W/2]]
      self.DrawPolygon(corners,x,y,phi)
      # TODO
    elif "MSTEP" in l:
      pass  # Draw nothing
    elif "MSLIT" in l:
      pass
    elif "MGAP" in l:
      pass
    elif "MCURVE" in l:
      pass
    elif "MCURVE2" in l:
      pass
    elif "MRSTUB" in l:
      # MRSTUB:MS10 _net18 Subst="Subst1" ri="1 mm" ro="10 mm" alpha="90"
      ri = element[3]
      ro = element[4]
      alpha = element[5]
      self.DrawMRSTUB(ri,ro,alpha,x,y,phi)
    elif "MBSTUB" in l:
      pass
    elif "MCFIL" in l:
      pass
    elif "MCLIN" in l:
      pass
    elif "MCOUPLED" in l:
      W = element[6]
      L = element[7]
      S = element[8]
      v21 = dot(R,v[1][0:2])
      self.DrawRectangleLEO(W,L,x,y,phi)
      self.DrawRectangleLEO(W,L,v21[0]+x,v21[1]+y,phi)
    elif "MSABND" in l:
      pass
    elif "MSOBND" in l:
      pass
    elif "MCROSS" in l:
      # elements.append(["MCROSS",l[0],l[1],l[2],l[3],l[4],W1,W2,W3,W4])
      W1 = element[6]
      W2 = element[7]
      W3 = element[8]
      W4 = element[9]
      x_m = max(W4,W2)
      y_m = max(W1,W3)
      v21 = dot(R,v[1][0:2])
      v31 = dot(R,v[2][0:2])
      v41 = dot(R,v[3][0:2])
      #self.fprint("MCROSS {}: phi = {}, v = {}, v21 = {}, v31 = {}, v41 = {}".format(element[1],phi,v,v21,v31,v41))
      self.DrawRectangleLEO(W1,x_m/2.0,x,y,phi)  # Node 1
      self.DrawRectangleLEO(W2,y_m/2.0,x+v21[0],y+v21[1],phi - 90)  # Node 2
      self.DrawRectangleLEO(W3,x_m/2.0,x+v31[0],y+v31[1],phi +180) # Node 3
      self.DrawRectangleLEO(W4,y_m/2.0,x+v41[0],y+v41[1],phi + 90) # Node 3
    elif "MCROSO" in l:
      pass
    elif "MSOP" in l:
      pass
    elif "VIAGND" in l:
      pass
    elif "VIA2" in l:
      pass

  def GetElementsUsingNet(self,net):
    connected = []
    for e in self.elements:
      L = len(self.GetVectors(e))
      for l in range(L):
        if net == e[2+l]:
          connected.append(e[1])
          break
    return connected

  def GenerateGerberElements(self):
    self.fprint("\nPlotting all elements with coordinates...")
    self.missed = []
    for e in self.elements:
      c = self.GetCoordinate(e[2])
      if c:
        x = c[1][0]
        y = c[1][1]
        phi = c[1][2]
        # Do final slave check
        slave = True
        for s in self.slaves:
          if (c[0] == s[0]) or (e[1] == s[1]):
            slave = False
        if slave:
          self.slaves.append([c[0],e[1]])
        # add extra 180 degrees is needed
        pair_not_in_slave_list = not self.InSlaveList(e[2],e[1])
        if pair_not_in_slave_list:
          phi = phi + 180
        self.WriteElement(e,x,y,phi)
      else:
        self.missed.append(e[1])
        # Report any elements that could not be connected
    if len(self.missed) > 0:
      self.fprint("Warning: These elements could not be connected! {}".format(self.missed),force=True)

  def ProcessNetlist(self):
    self.coordinates = []
    self.slaves = []
    self.ParseNetlist()
    while self.GetNextCoordinate(): pass

    # Do final slave check, this should add any connected elements missed the first time
    for e in self.elements:
      c = self.GetCoordinate(e[2])
      if c:
        # Do final slave check
        slave = True
        for s in self.slaves:
          if (c[0] == s[0]) or (e[1] == s[1]):
            slave = False
        if slave:
          self.slaves.append([c[0],e[1]])

    self.fprint("Coordinates: {}".format(self.coordinates))
    self.fprint("Slaves:      {}".format(self.slaves))
    self.GenerateGerberElements()

  # ----------------------------------------
  # Main function call
  # ----------------------------------------


'''code for finding the correct corresponding landmarks given BFI and
MRI - not run by itself, '''

import numpy as np
import pickle                   # for loading rigid reg affine

def orig_to_reg(landmark):
    '''takes a set of original landmarks in original pixel coordinates
    (i.e. [pixel HR BFI, voxel MRI]) and converts it to
    [BFI real coords (self reg), voxel real coords]'''
    pass

def get_new_landmarks(block, orig=False):
    '''returns the landmark corresponance between the blockface
    imaging and the MRI

    Takes block = {1, 2, 3, 4} argument plus the downsampling (default
    is 8)'''

    # downsampling doesnt matter because we convert all the landmarks
    # to real coordinates, so as long as the data has correct spacing
    # (which depends on the downsampling), we should be fine.  We do
    # some division by 8 (which is the normally downsampled data), but
    # thats because the affine registration was done on those images,
    # so photo_rigid_transformations relies on that grid

    # landmarks matching the high res data from
    # /home/sci/crottman/korenberg/data/photo/seg_high_res_crop/color/block2
    # and the MRI data from
    # /home/sci/crottman/korenberg/results/MRI/brain_seg.mha
    #
    # these are the landmarks given to me by Julie
    #
    # [pixel HR BFI, voxel MRI]
    # if block == 1:              # 12
    #     landmarks = [[[1826.0, 1754.5, 7470], [158.43, 22, 125.21]],
    #                  [[2238.5, 1716.0, 9780], [179.31, 32, 127.27]],
    #                  [[1639.0, 1133.0, 11820], [158.54, 37, 89.375]],
    #                  [[2398.0, 1668.0, 14910], [143.8, 53, 123.2]],
    #                  [[1994.0, 1492.0, 13110], [127.26, 43, 109.43]],
    #                  [[2668.0, 2512.0, 15570], [148.53, 56, 171.67]],
    #                  [[2532.0, 1986.0, 13440], [147.76, 47, 141]],
    #                  [[2096.0, 1478.0, 14070], [132.05, 47, 108.56]],
    #                  [[2372.0, 1008.0, 16860], [149.09, 53, 88.092]],
    #                  [[3193.5, 2241.0, 16710], [177.06, 59, 162.33]],
    #                  [[2397.0, 2235.0, 12780], [140.32, 46, 152.06]],
    #                  [[2662.0, 2658.0, 15570], [146.59, 56, 177.68]]
    #                  ]
    if block ==1:
        landmarks = [[[2522.60336538,1315.15771902,6871.10693359],[158.43, 22, 125.21]],
                     [[2987.42334402,1229.36865652,9881.06542969],[179.31, 32, 127.27]],
                     [[2438.39059161,577.50814637,12034.25976562],[158.54, 37, 89.375]],
                     [[2354.12433226,1311.07271635,14710.29980469],[143.8, 53, 123.2]],
                     [[1906.85867805,1171.23530983,12912.47460938],[127.26, 43, 109.43]],
                     [[2718.82291667,2143.2850227,15241.23730469],[148.53, 56, 171.67]],
                     [[2497.11044338,1654.34309896,13280.20605469],[147.76, 47, 141]],
                     [[2003.25730335,1151.70072115,13824.54931641],[132.05, 47, 108.56]],
                     [[2298.45469418,629.74238782,16997.55175781],[149.09, 53, 88.092]],
                     [[3182.02577457,1813.38330495,15916.63476563],[177.06, 59, 162.33]],
                     [[2393.5100828,1908.59584085,12689.05053711],[140.32, 46, 152.06]],
                     [[2722.94484509,2282.91720085,15419.27734375],[146.59, 56, 177.68]]]

    elif block == 2:            # 22
        landmarks = [[[1692.0, 1798.0, 5460], [122.54, 78, 131.64]],
                     [[1808.0, 788.0, 2940], [138.64, 71, 74.479]],
                     [[3440.0, 2412.0, 9960], [189.92, 102, 164.8]],
                     [[1692.0, 1734.0, 5940], [123.53, 80, 128.11]],
                     [[1764.0, 1830.0, 5850], [126.11, 80, 134.28]],
                     [[1392.0, 1569.0, 6600], [106.61, 82, 117.43]],
                     [[1192.0, 2012.0, 10320], [76.206, 91, 126.36]],
                     [[1800.0, 2410.0, 4200], [127.03, 75, 162.93]],
                     [[1392.0, 2720.0, 7650], [101.41, 83, 176.69]],
                     [[712.0, 1640.0, 8190], [73.153, 83, 116.31]],
                     [[1696.5, 2634.0, 5430], [118.11, 78, 178.81]],
                     [[1873.5, 2647.5, 11670], [108.84, 100, 167.84]],
                     [[1885.5, 2374.5, 11430], [109.71, 100, 152.28]],
                     [[1796.7, 2032.7, 10890], [107.55, 96, 133.46]],
                     [[1579.5, 1597.5, 11610], [99.954, 98, 106.04]],
                     [[1258.0, 2050.0, 2310], [101.68, 64, 140.72]],
                     [[1918.5, 1713.0, 8820], [132.3, 91, 127.57]],
                     [[1372.5, 2796.0, 6630], [101.52, 79, 182.85]],
                     [[1282.0, 970.0, 12540], [88.143, 100, 72.372]],
                     [[1716.0, 1570.0, 6480], [123.78, 82, 127.5]],
                     [[1790.0, 1818.0, 6360], [127.3, 82, 134.34]],
                     [[1358.0, 1940.0, 10230], [87.009, 91, 125.07]]
                     ]

    #### BLAKE NEW ATTEMPT AT LANDMARKS #####
    # elif block == 2:
    #     landmarks = [[[1688.0,1704.0,5130],[122.54,78,131.64]],
    #                  [[1872.0,592.0,3000],[138.64,71,74.479]],
    #                  [[1696.0,1616.0,5640],[123.53,80,128.11]],
    #                  [[1744.0,1728.0,5550],[126.11,80,134.28]],
    #                  [[1424.0,1472.0,6000],[106.61,82,117.43]],
    #                  [[808.0,1760.0,9810],[76.206,91,126.36]],
    #                  [[1672.0,2544.0,5220],[118.11,78,178.81]],
    #                  [[1472.0,2392.0,11670],[108.84,100,167.84]],
    #                  [[1456.0,2136.0,11190],[109.71,100,152.28]],
    #                  [[1400.0,1792.0,10410],[107.55,96,133.46]],
    #                  [[1200.0,1256.0,11340],[99.954,98,106.04]],
    #                  [[1288.0,1912.0,1890],[101.68,64,140.72]],
    #                  [[1360.0,2680.0,6390],[101.52,79,182.85]],
    #                  [[920.0,688.0,11310],[88.143,100,72.372]],
    #                  [[1704.0,1624.0,6180],[123.78,82,127.5]],
    #                  [[1760.0,1712.0,6030],[127.3,82,134.34]],
    #                  [[992.0,1752.0,9720],[87.009,91,125.07]]
    #                  ]

    #######################

    elif block == 3:
        landmarks = [[[1851.62879357,1991.99500033,3705.06347656],[120.45,118,131.66]],
                     [[1524.91439637,1126.08987714,3463.581604],[119.84,121,83.647]],
                     [[2782.59481838,2264.22305689,2796.15209961],[158.59,119,161.38]],
                     [[901.1676015,1244.17935363,4935.58862305],[87.865,122,76.284]],
                     [[1909.73646585,1925.96554226,4652.20556641],[123.25,120,130.49]],
                     [[2609.97662927,2834.66399573,5220.45776367],[141.11,125,187.6]],
                     [[1471.62680288,1129.20699786,2732.40942383],[116.62,116,84.199]],
                     [[2762.40110844,2390.66673344,1642.92230225],[154.47,114,169.05]]]

    # elif block == 3:            # 8
    #     landmarks = [[[1915.5, 1941.0, 3960], [120.45, 118, 131.66]],
    #                  [[1566.0, 1114.0, 3660], [119.84, 121, 83.647]],
    #                  [[2814.0, 2229.0, 2850], [158.59, 119, 161.38]],
    #                  [[950.0, 1212.0, 4680], [87.865, 122, 76.284]],
    #                  [[1960.0, 1908.0, 4620], [123.25, 120, 130.49]],
    #                  [[2804.0, 2600.0, 3570], [141.11, 121, 187.6]],
    #                  [[1530.0, 1130.0, 2850], [116.62, 116, 84.199]],
    #                  [[2788.0, 2394.0, 1710], [154.47, 114, 169.05]],
    #                  ]

    elif block == 4:            # 20
        # landmarks = [[[2866.0, 2644.0, 1950], [148.46, 132, 184.62]],
        #              [[1658.0, 2054.0, 5700], [105.51, 145, 126.21]],
        #              [[1944.0, 2282.0, 6000], [114.69, 145, 144.58]],
        #              [[3076.5, 1476.0, 9030], [192.09, 158, 136.95]],
        #              [[2034.0, 1388.0, 21270], [135.86, 207, 114.8]],
        #              [[2600.0, 2352.0, 9330], [143.9, 157, 161.09]],
        #              [[1431.0, 1312.5, 8100], [107.61, 156, 93.076]],
        #              [[2582.0, 1350.0, 20100], [165.34, 194, 111.29]],
        #              [[2362.0, 1046.0, 18270], [154.86, 201, 104.98]],
        #              [[898.0, 2026.0, 13200], [75.349, 179, 116.18]],
        #              [[2086.0, 1754.0, 1140], [133.67, 128, 125.02]],
        #              [[2278.5, 2412.0, 1440], [130.17, 128, 154.87]],
        #              [[1432.0, 1436.0, 6360], [103.74, 150, 96.354]],
        #              [[2288.0, 2292.0, 7710], [133.86, 151, 146.39]],
        #              [[1902.0, 1516.0, 13860], [123.94, 180, 115.41]],
        #              [[1798.0, 2586.0, 15600], [103.14, 178, 162.73]],
        #              [[2334.0, 1998.0, 8670], [142.76, 156, 135.61]],
        #              [[1686.0, 2314.0, 3960], [102.09, 138, 140.24]],
        #              [[1782.0, 1953.0, 4740], [121.03, 141, 139.15]],
        #              [[1588.0, 784.0, 12330], [125.43, 174, 71.346]],
        #              ]
        # these are not reversed:
        # return [[[-4978.7969255009493, -14291.195548170297, -10974.191228834581], [-49.0, 8.5, -34.7]],
        #         [[12955.285489863672, 6782.6667541970291, -9608.9022334155761], [-7.2, 8.5, 54.2]],
        #         [[-3529.4124992429142, -21992.693397096813, 21.610449345362213], [-71.0, 49.5, -37.5]],
        #         [[1375.0854059966568, -2424.3665112550225, -644.14053162552591], [-19.0, 41.9, 6.5]],
        #         [[-1904.9849544357239, 19756.728151140465, -9850.3999775772809], [62.5, 14.2, 31.5]],
        #         [[-19978.084988611834, 1193.8850080611674, -5171.9937461739564], [27.5, 34.7, -46.5]],
        #         [[4876.4172072069205, 12908.96310608848, 3440.3626957034357], [23.1, 59.2, 35.5]],
        #         [[9048.4258535122353, -3346.5882992785419, -1034.3280093132544], [-31.4, 38.6, 31.5]],
        #         [[-18786.106171386909, 4379.237425767692, -11754.583853417926], [30.0, 2.5, -46.9]],
        #         [[-3987.5298698229044, 1771.7456071262263, -11229.664187291755], [5.5, 2.5, -3.1]],
        #         [[4053.8534067774444, 18364.900285395477, -9103.886061444251], [47.0, 13.5, 49.5]],
        #         [[-4253.4490262881664, 1858.4383859028028, -10978.167005247227], [5.9, 2.5, -3.6]],
        #         [[-4424.9749261128964, -14276.880981292834, -10691.448917965858], [-49.8, 7.5, -31.7]],
        #         [[-7431.4216598651183, 12080.35048564759, 5666.7585269799256], [44.3, 64.0, -6.5]],
        #         [[-11750.706201447651, -6425.4834199773559, 2206.6704343840447], [-15.9, 53.8, -37.5]],
        #         [[-9553.0213692492798, -8271.5033718832747, 9617.0907338800625], [-26.4, 82.5, -30.5]],
        #         [[5646.7187209368558, -5709.066465405941, 1405.149115939319], [-32.7, 48.5, 18.8]],
        #         [[-3045.289844922052, -15641.47673776804, 2829.7547401188876], [-48.7, 62.5, -21.5]],
        #         [[1714.4211894935061, -2015.5792753776332, -1241.3595924584788], [-17.8, 40.5, 9.4]],
        #         [[9630.4167197410025, 14435.741532626829, 3212.6170714054715], [18.9, 51.5, 58.5]]
        #     ]
        # these are properly reversed:
        return [[[-9305.5569124382746, -926.00166826035411, -1138.4258596439486], [20.6, 44.5, 8.6]],
                [[5111.9588267015597, 14394.506928486426, 3027.1478194585925], [-19.2, 52.5, -50.8]],
                [[5319.0894148311518, -5334.2601974636691, 364.14433790943804], [-31.0, 45.5, 17.4]],
                [[-1540.0662948951845, 9674.0229935199168, 8068.2477026551223], [1.5, 75.5, -27.6]],
                [[-11142.733858577409, 1.7009214676677402, 7385.7016125897044], [23.9, 75.5, 5.5]],
                [[6554.2612007950547, 7535.3030659200731, 10931.232554415083], [-31.3, 84.5, -25.4]],
                [[-4386.1442975165064, 8445.52207500855, 11032.014096545239], [8.4, 85.5, -20.4]],
                [[14983.7843151907, 687.38589765583129, 1458.8393813034727], [-59.7, 49.5, -11.3]],
                [[2710.4518412050411, -14587.200900390035, 1278.0350246270846], [-28.6, 48.5, 54.1]],
                [[11305.014583188564, 12733.418672147429, -12354.181250633284], [-39.3, -3.5, -53.8]],
                ]

    else:
        raise Exception("invalid block number")

    if orig:
        return landmarks

    # Convert to LR landmarks
    #
    # landmarks matching the low res 8 data from (w/ numpy x/y)
    # /home/sci/crottman/korenberg/data/photo/seg_low_res_crop/color/block2
    # and the MRI data from
    # /home/sci/crottman/korenberg/results/MRI/brain_seg.mha
    #
    for ptpair in landmarks:
        ptpair[0][0:2] = ptpair[0][-2:-4:-1] # switch x and y
        ptpair[0][0] /= 8.0
        ptpair[0][1] /= 8.0
    f = open("photo_rigid_transformations_" + str(block) + ".pkl", 'r')
    Adict = pickle.load(f)
    f.close()
    # Transform landmarks by rigid transform from photorigidtransformations
    for lm in landmarks:
        if lm[0][2] in Adict.keys():
            A = Adict[lm[0][2]]
            origin = Adict['origin']
            B = np.array(A)
            # print "landmark ", i, "found!!!"
            # print "previous landmark: ", lm[0]
            # Apply affine transformation to landmark
            lm[0][0:2] = np.dot(B, [lm[0][0]-origin[0], lm[0][1]-origin[1], 1.0])[0:2]
            lm[0][0] += origin[0]
            lm[0][1] += origin[1]
            # print "new landmark: ", lm[0], "\n"

            # [block2_reg_blanks pixels, MRI voxels]

            # convert to real coordinates
            # [block2_reg_blanks Real, MRI voxels]

    # Convert to World Coordinates
    for lm in landmarks:
        # the spacing for the HR BFI data is [117, 117, 30]
        lm[0][0] = lm[0][0]*117
        lm[0][1] = lm[0][1]*117

    bfspacing = np.array([117, 117, 30])
    if block == 1:
        bfsize = np.array([480, 480, 585])
    elif block == 2:
        bfsize = np.array([480, 480, 419])
    elif block == 3:
        bfsize = np.array([480, 480, 165])
    elif block == 4:
        bfsize = np.array([480, 480, 874])

    # origins are center of the block
    bforigin = -(bfsize-1)/2.0*bfspacing
    mriorigin = -255.0/2.0*np.array([1.0, 1.0, 1.0])

    for lm in landmarks:
        lm[0][0] += bforigin[0]
        lm[0][1] += bforigin[1]
        lm[0][2] += bforigin[2]
        lm[1][0] = round(lm[1][0] + mriorigin[0], 2) # just an annoyance :(
        lm[1][1] = round(lm[1][1] + mriorigin[1], 2)
        lm[1][2] = round(lm[1][2] + mriorigin[2], 2)

    return landmarks


from typing import Tuple, Optional, List
import warnings
import ctypes
import operator
from functools import reduce
import torch
import numpy as np
from dqc.hamilton.intor.lcintwrap import LibcintWrapper
from dqc.hamilton.intor.utils import np2ctypes, int2ctypes, CGTO, CPBC, \
                                     c_null_ptr
from dqc.hamilton.intor.pbcintor import PBCIntOption, _check_and_set_pbc, \
                                        _get_default_options, _get_default_kpts, \
                                        _concat_atm_bas_env
from dqc.utils.types import get_complex_dtype
from dqc.utils.pbc import estimate_ovlp_rcut
from dqc.hamilton.intor.lattice import Lattice
from dqc.hamilton.intor.namemgr import IntorNameManager

__all__ = ["pbcft_int1e", "pbcft_overlap"]

# Fourier transform integrals
def pbcft_int1e(shortname: str, wrapper: LibcintWrapper,
                other: Optional[LibcintWrapper] = None,
                gvgrid: Optional[torch.Tensor] = None,
                kpts: Optional[torch.Tensor] = None,
                options: Optional[PBCIntOption] = None):
    r"""
    Performing the periodic boundary condition (PBC) on 1-electron Fourier
    Transform integrals, i.e.

    $$
    \sum_\mathbf{T} e^{-i \mathbf{k}\cdot\mathbf{T}} \int \exp(-i\mathbf{G}\cdot\mathbf{r})
    \phi_i(\mathbf{r}) \phi_j(\mathbf{r}-\mathbf{T})\ \mathrm{d}\mathbf{r}
    $$

    Arguments
    ---------
    shortname: str
        The shortname of the integral (i.e. without the prefix `int1e_` or else)
    wrapper: LibcintWrapper
        The environment wrapper containing the basis
    other: Optional[LibcintWrapper]
        Another environment wrapper containing the basis. This environment
        must have the same complete environment as `wrapper` (e.g. `other` can be
        a subset of `wrapper`). If unspecified, then `other = wrapper`.
    gvgrid: Optional[torch.Tensor]
        The reciprocal coordinate of $\mathbf{G}$ with shape `(nggrid, ndim)`.
        If unspecified, then it is assumed to be all zeros.
    kpts: Optional[torch.Tensor]
        k-points where the integration is supposed to be performed. If specified,
        it should have the shape of `(nkpts, ndim)`. Otherwise, it is assumed
        to be all zeros.
    options: Optional[PBCIntOption]
        The integration options. If unspecified, then just use the default
        value of `PBCIntOption`.

    Returns
    -------
    torch.Tensor
        A complex tensor representing the 1-electron integral with shape
        `(nkpts, *ncomp, nwrapper, nother, nggrid)` where `ncomp` is the Cartesian
        components of the integral, e.g. `"ipovlp"` integral will have 3
        components each for x, y, and z.
    """

    # check and set the default values
    other1 = _check_and_set_pbc(wrapper, other)
    options1 = _get_default_options(options)
    kpts1 = _get_default_kpts(kpts, dtype=wrapper.dtype, device=wrapper.device)
    gvgrid1 = _get_default_kpts(gvgrid, dtype=wrapper.dtype, device=wrapper.device)

    assert isinstance(wrapper.lattice, Lattice)  # check if wrapper has a lattice
    return _PBCInt2cFTFunction.apply(
        *wrapper.params,
        *wrapper.lattice.params,
        gvgrid1,
        kpts1,
        [wrapper, other1],
        IntorNameManager("int1e", shortname), options1)

# shortcuts
def pbcft_overlap(wrapper: LibcintWrapper,
                  other: Optional[LibcintWrapper] = None,
                  gvgrid: Optional[torch.Tensor] = None,
                  kpts: Optional[torch.Tensor] = None,
                  options: Optional[PBCIntOption] = None):
    return pbcft_int1e("ovlp", wrapper, other, gvgrid, kpts, options)

################# torch autograd function wrappers #################
class _PBCInt2cFTFunction(torch.autograd.Function):
    # wrapper class for the periodic boundary condition 2-centre integrals
    @staticmethod
    def forward(ctx,  # type: ignore
                # basis params
                allcoeffs: torch.Tensor, allalphas: torch.Tensor, allposs: torch.Tensor,
                # lattice params
                alattice: torch.Tensor,
                # other parameters
                gvgrid: torch.Tensor,
                kpts: torch.Tensor,
                # non-tensor parameters
                wrappers: List[LibcintWrapper], int_nmgr: IntorNameManager,
                options: PBCIntOption) -> torch.Tensor:
        # allcoeffs: (ngauss_tot,)
        # allalphas: (ngauss_tot,)
        # allposs: (natom, ndim)

        out_tensor = PBCFTIntor(int_nmgr, wrappers, gvgrid, kpts, options).calc()
        ctx.save_for_backward(allcoeffs, allalphas, allposs, alattice, gvgrid, kpts)
        ctx.other_info = (wrappers, int_nmgr, options)
        return out_tensor

    @staticmethod
    def backward(ctx, grad_out: torch.Tensor) -> Tuple[Optional[torch.Tensor], ...]:  # type: ignore
        raise NotImplementedError("gradients of PBC 2-centre FT integrals are not implemented")

################# integrator object (direct interface to lib*) #################
class PBCFTIntor(object):
    def __init__(self, int_nmgr: IntorNameManager, wrappers: List[LibcintWrapper],
                 gvgrid_inp: torch.Tensor, kpts_inp: torch.Tensor, options: PBCIntOption):
        # This is a class for once integration only
        # I made a class for refactoring reason because the integrals share
        # some parameters
        # No gradients propagated in the methods of this class

        assert len(wrappers) > 0
        wrapper0 = wrappers[0]
        kpts_inp_np = kpts_inp.detach().numpy()  # (nk, ndim)
        GvT = np.asarray(gvgrid_inp.detach().numpy().T, order="C")  # (ng, ndim)
        opname = int_nmgr.get_ft_intgl_name(wrapper0.spherical)
        lattice = wrapper0.lattice
        assert isinstance(lattice, Lattice)

        # get the output's component shape
        comp_shape = int_nmgr.get_intgl_components_shape()
        ncomp = reduce(operator.mul, comp_shape, 1)

        # estimate the rcut and the lattice translation vectors
        coeffs, alphas, _ = wrapper0.params
        rcut = estimate_ovlp_rcut(options.precision, coeffs, alphas)
        ls = np.asarray(lattice.get_lattice_ls(rcut=rcut))

        self.int_type = int_nmgr.int_type
        self.wrappers = wrappers
        self.GvT = GvT
        self.kpts_inp_np = kpts_inp_np
        self.opname = opname
        self.dtype = wrapper0.dtype
        self.device = wrapper0.device
        self.comp_shape = comp_shape
        self.ncomp = ncomp
        self.ls = ls
        self.options = options

        # this class is meant to be used once
        self.integral_done = False

    def calc(self) -> torch.Tensor:
        assert not self.integral_done
        self.integral_done = True
        if self.int_type == "int1e":
            return self._int2c()
        else:
            raise ValueError("Unknown integral type: %s" % self.int_type)

    def _int2c(self) -> torch.Tensor:
        # 2-centre integral
        # this function works mostly in numpy
        # no gradients propagated in this function (and it's OK)
        # this function mostly replicate the `ft_aopair_kpts` function in pyscf
        # https://github.com/pyscf/pyscf/blob/master/pyscf/pbc/df/ft_ao.py
        # https://github.com/pyscf/pyscf/blob/c9aa2be600d75a97410c3203abf35046af8ca615/pyscf/pbc/df/ft_ao.py#L52
        assert len(self.wrappers) == 2

        # if the ls is too big, it might produce segfault
        if (self.ls.shape[0] > 1e6):
            warnings.warn("The number of neighbors in the integral is too many, "
                          "it might causes segfault")

        # libpbc will do in-place shift of the basis of one of the wrappers, so
        # we need to make a concatenated copy of the wrapper's atm_bas_env
        atm, bas, env, ao_loc = _concat_atm_bas_env(self.wrappers[0], self.wrappers[1])
        i0, i1 = self.wrappers[0].shell_idxs
        j0, j1 = self.wrappers[1].shell_idxs
        nshls0 = len(self.wrappers[0].parent)
        shls_slice = (i0, i1, j0 + nshls0, j1 + nshls0)

        # get the lattice translation vectors and the exponential factors
        expkl = np.asarray(np.exp(1j * np.dot(self.kpts_inp_np, self.ls.T)), order='C')

        # prepare the output
        nGv = self.GvT.shape[-1]
        nkpts = len(self.kpts_inp_np)
        outshape = (nkpts,) + self.comp_shape + tuple(w.nao() for w in self.wrappers) + (nGv,)
        out = np.empty(outshape, dtype=np.complex128)

        # do the integration
        cintor = getattr(CGTO(), self.opname)
        eval_gz = CPBC().GTO_Gv_general
        fill = CPBC().PBC_ft_fill_ks1
        drv = CPBC().PBC_ft_latsum_drv
        p_gxyzT = c_null_ptr()
        p_mesh = (ctypes.c_int * 3)(0, 0, 0)
        p_b = (ctypes.c_double * 1)(0)
        drv(cintor, eval_gz, fill,
            np2ctypes(out),  # ???
            int2ctypes(nkpts),
            int2ctypes(self.ncomp),
            int2ctypes(len(self.ls)),
            np2ctypes(self.ls),
            np2ctypes(expkl),
            (ctypes.c_int * len(shls_slice))(*shls_slice),
            np2ctypes(ao_loc),
            np2ctypes(self.GvT),
            p_b, p_gxyzT, p_mesh,
            int2ctypes(nGv),
            np2ctypes(atm), int2ctypes(len(atm)),
            np2ctypes(bas), int2ctypes(len(bas)),
            np2ctypes(env))

        out_tensor = torch.as_tensor(out, dtype=get_complex_dtype(self.dtype),
                                     device=self.device)
        return out_tensor