Waveguide Resizing without PCell

edited August 2017 in Python scripting
Hello Matthias,

I have written a waveguide routing routine in Python using a path as input. I wanted it to be general, such that you can specify an arbitrary amount of layers, widths, and offsets from the WG center so implementing slot, ridge, or other more complex designs would be easier.

Because I wanted to define an arbitrary number of layers, I could not (to my knowledge) use a PCell to implement this, but I would still like to be able to resize the waveguide by modifying the underlying path it was made from using the "Partial" command. Is there a way to do this without using a PCell? I have stored the path as a property of the waveguide cell.

Thank you,

Brett

Comments

  • edited November -1

    bruxillensis,

    Perhaps you know but I'll point out that some of the functionality you require is built-in. Draw a path, then choose Edit > Selection > Convert to PCell > Basic.ROUND_PATH. To modify the path after that, choose the Partial tool and drag the path around. Of course you can't do arbitrary number of layers, width, slot, ridge, etc.

    I haven't looked at it recently but Lukas Chrostowski made some super impressive photonics tools (link to PDK) including a waveguide routing tool. However, last I checked the generated waveguide weren't reconfigurable. So to re-route his waveguide you just delete the old one, move the original wire, then convert the wire to waveguide again.

    One final option. A while ago I wrote a script that basically replicates the Basic.ROUND_PATH feature. It's very limited (can only do manhattan geometry, etc). But the resulting waveguide is reconfigurable so you can drag it around with the Partial tool. It did indeed use PCells, with the guiding shape (the one you drag) on the so-called "Guiding shapes layer" which is a special layer. Maybe you can use that as a starting point and combine it with Lukas's more in-depth code. I'll try to dig it up in the next couple of days, sorry I don't have it at this computer.

    David

  • edited August 2017

    Here you go. I warn you the code is ugly and definitely not my best work...

    I just realized that my code is ruby while you have been using python. Anyway I hope it's helpful.

    Also you said you think you can't use PCell to implement an arbitrary number of layers -- you can. Just define another layer in the param(...) lines and draw it using a cell.shapes(other_layer_index).insert(path, other_width).

    If it's not clear how to add another layer in to this code to make a rib waveguide for instance, let me know.

    To use it: Either:

    (A) Draw then select a path. Edit > Selection > Convert to PCell > WgLib:RoundedPath

    or

    (B) You can also just call this from python/ruby code like you're instantiating a regular PCell


    ## (PCell) Rounded Path.lym
    ## By davidnhutch
    ## 
    ## A rounded path. Only works with manhattan input paths for now.
    ## To use: Select a path. Edit > Selection > Convert to PCell > WgLib.RoundedPath.
    ## Then drag edges around using the "Partial" tool. Don't drag vertices since
    ## this will often break the manhattan geometry.
    ## 
    ## Set this script to run on startup.
    
    module WgLib
      include RBA
    
      WgLib.constants.member?(:RoundedPath) && remove_const(:RoundedPath)
      WgLib.constants.member?(:WgLib) && remove_const(:WgLib)
    
      class RoundedPath < PCellDeclarationHelper
        include RBA
    
        def initialize
          super
    
          default_width = 1.0
          default_radius = 10.0
          default_npoints = 64
          len = 100.0
          points_backbone = [[0.0,0.0],[len,0.0],[len,len],[2*len,len]]
    
          points_x = points_backbone.transpose[0]
          points_y = points_backbone.transpose[1]
          points_x.map! { |v| "#{v}" }
          points_y.map! { |v| "#{v}" }
          default_numpts = points_backbone.length
          dpoints_backbone = []
          points_backbone.each { |p| dpoints_backbone.push(DPoint.new(p[0],p[1])) }
    
          param(:width, TypeDouble, "Width", :default => default_width, :unit => "um")
          param(:radius, TypeDouble, "Radius", :default => default_radius, :unit => "um")
          param(:npoints, TypeInt, "Number of points", :default => default_npoints)     
          param(:l, TypeLayer, "Layer", :default => LayerInfo::new(1, 0))
    
          param(:xcoords, TypeList, "x coords", :default => points_x, :hidden => true)
          param(:ycoords, TypeList, "y coords", :default => points_y, :hidden => true)
          param(:xcoordsu, TypeList, "x coords handle tracker", :default => points_x, :hidden => true)
          param(:ycoordsu, TypeList, "y coords handle tracker", :default => points_y, :hidden => true)
    
          # It seems the shape has to be a DPath. A Path doesn't seem to work.
          param(:s, TypeShape, "", :default => DPath::new(dpoints_backbone, default_width))
    
        end
    
        def display_text_impl
          "RoundedPath(R=#{radius.to_s},N=#{npoints.to_s})"
        end
    
        def coerce_parameters_impl  
          dbu = layout.dbu
    
          # Get the points that make up the wire's backbone
          ptarr = []
          s.each_point { |p| ptarr << p } # These are DPoints, because they come from the guiding shape which is a DPath
          npoints = ptarr.length # If the number of points has changed, update the npoints parameter.
    
          # Set the x and y coords
          x = []
          y = []
          dpath_arr = []
          ptarr.length.times { |i|
            x << "#{ptarr[i].x}"
            y << "#{ptarr[i].y}"
            xx = eval(x[i])
            yy = eval(y[i])
            dpath_arr << DPoint.new(xx,yy)
          }
          set_xcoords(x)
          set_ycoords(y)
          set_xcoordsu(x)
          set_ycoordsu(y)
          set_s(DPath.new(dpath_arr, width)) 
        end
    
        def can_create_from_shape_impl
          shape.is_path?
        end
    
        def parameters_from_shape_impl
          dbu = layout.dbu
    
          # Get the backbone points from the guiding shape
          ptarr = []
          shape.each_point { |p| ptarr << p }
          npoints = ptarr.length
    
          # Set the x and y coords
          x = []
          y = []
          dpath_arr = []
          ptarr.length.times { |i|
            x << "#{ptarr[i].x * dbu}"
            y << "#{ptarr[i].y * dbu}"
            xx = eval(x[i])
            yy = eval(y[i])
            dpath_arr << DPoint.new(xx,yy)
          }
          set_xcoords(x)
          set_ycoords(y)
          set_xcoordsu(x)
          set_ycoordsu(y)
    
          width = shape.path.width*dbu
          set_s(DPath.new(dpath_arr, width))
          set_width(width)
          set_l(layout.get_info(layer))
        end
    
        def transformation_from_shape_impl
          # Because it's a path, all its points are absolute, so there is no need 
          # for a transformation. So we set the transformation to the trivial one.
          Trans.new
        end
    
        # A custom function follows
        def get_rbend_curved_wg(pts_backbone, r, curve_num_pts)
          dbu = layout.dbu
          r = r/dbu
          # Get needed parameters
          i = 0
          pts = pts_backbone
          num_pts_backbone = pts_backbone.length
    
          curved_path=[]
          curved_path[0]=pts[0] # Write the first vertex
    
          if num_pts_backbone != 2 # As long as it's not a straight wire...
            counter = 0
    
            # In the following block we get a vector that includes the prev pt, the 
            # curr pt, and the next pt. Need all three to calculate the curve
            pts.each_cons(3) { |pcn|
              counter += 1
              p = pcn[0] # Previous
              c = pcn[1] # Current
              n = pcn[2] # Next
    
              pi=Math::PI
              # Figure out if the current line segment is traveling left to right,
              # top to bottom, etc...
              if p.x<c.x && p.y==c.y && c.x==n.x && c.y<n.y
                theta_start = 3*pi/2
                theta_end = 2*pi;
                dx = -r
                dy = r
              elsif p.x<c.x &&p.y==c.y && c.x==n.x && c.y>n.y
                theta_start = pi/2
                theta_end = 0
                dx = -r
                dy = -r
              elsif p.x==c.x && p.y<c.y && c.x<n.x && c.y==n.y
                theta_start = pi
                theta_end = pi/2
                dx = r
                dy = -r
              elsif p.x==c.x && p.y>c.y && c.x<n.x && c.y==n.y
                theta_start = pi
                theta_end = 3*pi/2
                dx = r
                dy = r
              elsif p.x==c.x && p.y>c.y && c.x>n.x && c.y==n.y
                theta_start = 2*pi
                theta_end = 3*pi/2
                dx = -r
                dy = r
              elsif p.x == c.x && p.y<c.y && c.x>n.x &&c.y==n.y
                theta_start = 0
                theta_end = pi/2
                dx = -r
                dy = -r
              elsif p.x>c.x && p.y==c.y && c.x==n.x && c.y>n.y
                theta_start = pi/2
                theta_end = pi
                dx = r
                dy = -r
              elsif p.x>c.x && p.y==c.y && c.x==n.x && c.y<n.y
                theta_start = 3*pi/2
                theta_end = pi
                dx = r
                dy = r
              else
                err_msg = "This shouldn't happen. You may have dragged one of the "\
                          "points resulting in a non-manhattan geometry. Press "\
                          "Undo, then drag an edge instead."
                MessageBox.info("Error", err_msg, MessageBox.b_ok)
              end
    
              # Make a linearly spaced array from theta_start to theta_end with 
              # curve_num_pts number of points
              dtheta = (theta_end-theta_start)/(curve_num_pts-1)
              theta = []
              for j in 0..curve_num_pts-1
                theta[j] = j*dtheta + theta_start
              end
    
              # Loop through each new curvy point for a given vertex
              theta.each { |angle|
                curved_path_x = (c.x + dx + r*Math::cos(angle))
                curved_path_y = (c.y + dy + r*Math::sin(angle))
                curved_path[i+1] = RBA::Point.new(curved_path_x,curved_path_y) 
                i+=1
              }
            }
            curved_path[i] = pts[pts.length - 1] #write the final vertex
    
            # Repair first vertex
            pt0 = pts[0]
            pt1 = pts[1]
            pt3 = pts[2]
    
            c0 = curved_path[0]
            c1 = curved_path[1]
    
            if (pt0.x == pt1.x && pt0.y < pt1.y && pt1.x < pt3.x && pt1.y == pt3.y) #if starts D->U, then bends to become L->R
              curved_path[1]   = Point.new(c0.x,c1.y)
            elsif (pt0.x == pt1.x && pt0.y > pt1.y && pt1.x > pt3.x && pt1.y == pt3.y) #if starts U->D, then bends to become R->L
              curved_path[1]   = Point.new(c0.x,c1.y)
            elsif (pt0.y == pt1.y && pt0.x < pt1.x && pt1.x == pt3.x && pt1.y < pt3.y) #if starts L->R, then bends to become D->U
              curved_path[1]   = Point.new(c1.x,c0.y) 
            elsif (pt0.y == pt1.y && pt0.x > pt1.x && pt1.x == pt3.x && pt1.y > pt3.y) #if starts R->L, then bends to become U->D
              curved_path[1]   = Point.new(c1.x,c0.y)
            elsif (pt0.x == pt1.x && pt0.y > pt1.y && pt1.x < pt3.x && pt1.y == pt3.y)  #if starts U->D, then bends to become L->R
              curved_path[1]   = Point.new(c0.x,c1.y)
            elsif (pt0.x == pt1.x && pt0.y < pt1.y && pt1.x > pt3.x && pt1.y == pt3.y) #if starts D->U, then bends to become R->L
              curved_path[1]   = Point.new(c0.x,c1.y)
            elsif (pt0.y == pt1.y && pt0.x > pt1.x && pt1.x == pt3.x && pt1.y < pt3.y) #if starts R->L, then bends to become D->U
              curved_path[1]   = Point.new(c1.x,c0.y)
            elsif (pt0.y == pt1.y && pt0.x < pt1.x && pt1.x == pt3.x && pt1.y > pt3.y) #if starts L->R, then bends to become U->D
              curved_path[1]   = Point.new(c1.x,c0.y)
            else
              p "First vertex is none of these; #{pt0.y} == #{pt1.y} && #{pt0.x} < #{pt1.x} && #{pt1.x} == #{pt3.x} && #{pt1.y} > #{pt3.y}"
            end
    
            # Repair last vertex
            pt0 = pts[pts.length - 3]
            pt1 = pts[pts.length - 2]
            pt3 = pts[pts.length - 1]
    
            ci1= curved_path[i-1]
            ci = curved_path[i]
    
            if (pt0.x == pt1.x && pt0.y < pt1.y && pt1.x < pt3.x && pt1.y == pt3.y) #if starts D->U, then bends to become L->R
              curved_path[i-1] = Point.new(ci1.x,ci.y)
            elsif (pt0.x == pt1.x && pt0.y > pt1.y && pt1.x > pt3.x && pt1.y == pt3.y) #if starts U->D, then bends to become R->L
              curved_path[i-1] = Point.new(ci1.x,ci.y)
            elsif (pt0.y == pt1.y && pt0.x < pt1.x && pt1.x == pt3.x && pt1.y < pt3.y) #if starts L->R, then bends to become D->U
              curved_path[i-1] = Point.new(ci.x,ci1.y)  
            elsif (pt0.y == pt1.y && pt0.x > pt1.x && pt1.x == pt3.x && pt1.y > pt3.y) #if starts R->L, then bends to become U->D
              curved_path[i-1] = Point.new(ci.x,ci1.y)
            elsif (pt0.x == pt1.x && pt0.y > pt1.y && pt1.x < pt3.x && pt1.y == pt3.y)  #if starts U->D, then bends to become L->R
              curved_path[i-1] = Point.new(ci1.x,ci.y)
            elsif (pt0.x == pt1.x && pt0.y < pt1.y && pt1.x > pt3.x && pt1.y == pt3.y) #if starts D->U, then bends to become R->L
              curved_path[i-1] = Point.new(ci1.x,ci.y)
            elsif (pt0.y == pt1.y && pt0.x > pt1.x && pt1.x == pt3.x && pt1.y < pt3.y) #if starts R->L, then bends to become D->U
              curved_path[i-1] = Point.new(ci.x,ci1.y)
            elsif (pt0.y == pt1.y && pt0.x < pt1.x && pt1.x == pt3.x && pt1.y > pt3.y) #if starts L->R, then bends to become U->D
              curved_path[i-1] = Point.new(ci.x,ci1.y)  
            else
              p "Final vertex is none of these; #{pt0.y} == #{pt1.y} && #{pt0.x} < #{pt1.x} && #{pt1.x} == #{pt3.x} && #{pt1.y} > #{pt3.y}"
            end
    
          else # If there are only two points in the wire
            curved_path[0] = pts[0]
            curved_path[1] = pts[1]
          end
          curved_path
        end
    
        def produce_impl
          dbu = layout.dbu
          set_xcoordsu(xcoords)
          set_ycoordsu(ycoords)
          # Fetch the parameters first
          xuarr = []
          yuarr = []
          numwirepts = xcoords.length
          numwirepts.times { |i|
            xuarr.push(xcoords[i])
            yuarr.push(ycoords[i])
          }
          xuarr.map! { |v| eval(v) / dbu }
          yuarr.map! { |v| eval(v) / dbu }
    
          ptarr = []
          numwirepts.times { |i| ptarr.push(Point.from_dpoint(DPoint::new(xuarr[i],yuarr[i]))) }
          ptarrcurved = get_rbend_curved_wg(ptarr, radius, npoints)
          cell.shapes(l_layer).insert(Path.new(ptarrcurved,width/dbu))
    
        end
      end
    
      class WgLib < Library
        def initialize  
          self.description = "Waveguide library"
          layout.register_pcell("RoundedPath", RoundedPath::new)
          register("WgLib")
        end
      end
      WgLib::new
    end
    
  • edited August 2017
    David,

    Thank you for the code, Ruby totally is fine, I'll see what I can come up with based on this. To some of your points: what I am working on is an extension of Lukas' waveguide routing tool, but like his, is not reconfigurable after the path is converted. To the other part about using a PCell, I have implemented it so the user can decide the number of layers at run-time, here https://github.com/lukasc-ubc/SiEPIC_EBeam_PDK/issues/159 is our discussion of the project with some pictures. It's this sort of configurable option that I wasn't able to achieve with a PCell since the number of parameters are predefined. If you know of a way to do this, I would very much like to learn.

    Brett
  • edited November -1

    I see. You want to have an arbitrary number of layers. I was thinking you wanted to "bake", say, 3 layers in to the code. If you can bake 3-4 layers in to the code I'd recommend that because it's easier (just a few lines added to my code above would do that. If that's not obvious then let me know and I'll show you which lines to repeat to hard-bake more layers in to the code.

    However if you really need "n" layers you can use the following workaround

    Make one of the parameters be a list of widths, like this:

    param(:widths, TypeList, "Widths: [outer0, inner0, outer1, inner1, ...]", :default => default_list)
    

    Then in the list you have pairs of widths. For example if you want the zeroth wire/waveguide to be a solid waveguide 1um wide you push 1.0 to the list then push 0.0 to the list. If you want the next waveguide to be a rib waveguide (i.e. drawn above and below, but nothing drawn in the waveguide center) that is 0.2um wide, and then drawn from +/- 0.1um until a total outer width 5um wide, you push 5.0 (the outer width) to the list and then 0.2 (the inner width). Continue pushing your outer width then your inner width. Anyway eventually your list is:

    [1.0, 0.0, 5.0, 0.2, ...]
    

    Now iterate through pairs of points. In the first loop iteration you have the 1.0 and 0.0 number for instance. You make a curved Path object for each. So, for the first two you make one curved path that is 1.0 wide and one curved path that is 0.0 wide. This is easy because the backbone xy points don't change, you just instantiate two paths instead of one. You don't place the paths, you just create the objects. Now let's call those two shapes you just created shape_outer and shape_inner. Then you do a boolean subtraction of the two.

    ep = RBA::EdgeProcessor::new
    outer_minus_inner = ep.boolean_p2p([shape_outer],[shape_inner], RBA::EdgeProcessor::ModeANotB, false, false)
    

    In this case (the 1.0 and 0.0 case, in our first loop iteration), you just get a solid wire 1um wide because shape_inner is 0um wide wire so it can't subtract anything from the 1um wide wire. Next you place your resultant shape outer_minus_inner.

    Then keep looping. On the next loop iteration you have a handle on the 5.0 and the 0.2 number. Again you create two curved path objects and do outer_minus_inner boolean operation on them, then place the resultant shape outer_minus_inner.

    And so forth.

    You now have an arbitrary number of layers of lightguides.

    You could just put them on successive layers (so the first one goes on layer 0, the next one on layer 1, ...). Or, if you have certain layers you want them on, then just make another param call:

    param(:gds_layer_numbers, TypeList, "GDS layer numbers", :default => default_layer_numbers)
    

    So you may feed it a list of integers like:

    [26, 54, ...]
    

    Now you can place your zeroth wire on GDS layer number 26, and your first wire on GDS layer number 54, and so on.

    All this can be done and still maintaining the ability to drag wires around shown in the original code above.

    Hopefully that helps. Please do tell us when you have finished, and post the code here or at least a link to it.

  • edited November -1
    Hey David,

    Thank you for the feedback. I wasn't able to find a TypeList definition in the documentation! I was searching for TypeArray, forgot I was using Python I guess. I will definitely post an update once complete, your answers have been very helpful!

    Brett
Sign In or Register to comment.