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.
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.
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
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.
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:
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:
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!