21 November 2019 at 18:54 #5849paulMember
Is it possible to maintain the curvature of the bend constant with
Ronald I have used your code with
viper()(here)to write a function returning a tapered bend along an arc with specified curvature and angle.
It works well but
viper()returns only a Polygon, which means that it does not integrate well with Interconnects or Xsections functions (i.e: I cannot easily use this tapered bend with pins, or cross-sections).
That’s why I think that using
cobra()would be more elegant and powerful but as you can see in the example below, I haven’t figured out how to keep the curvature constant throughout. This notably gives funny results for small angles.
import nazca as nd import numpy as np import nazca as nd from nazca.interconnects import Interconnect def tapered_bend(w1, w2, a, r): """Returns a bent taper. Bent section is defined as an arc bend of angle a and radius r. Tapered width is defined using nd.util.viper(). The width varies linearly from w1 to w2 Args: w1: taper start width w2: taper end width a: angle of the taper end edge to the horizontal r: radius of curvature of the taper Returns: A Cell containing the taper centred at (0,w1/2) """ def x(t): return r*np.cos(t*a/180*np.pi) def y(t): return r*np.sin(t*a/180*np.pi) def w(t): return w1-(w1-w2)*t with nd.Cell(name='taper_%s_%s'%(a,r)) as taper: # N is the number of points, x, y, w functions of one parameter. xygon = nd.util.viper(x, y, w , N=200) # shifts the polygon by y=r and rotate by -90deg to have the left edge # positionned at x=0 nd.Polygon(points=xygon, layer=2).put(0,r,-90) return taper if __name__ == "__main__": w1=10 w2=2 a=10 r=100 ic = Interconnect(width=2.0, radius=10.0) tapered_bend(w1, w2, a, r).put(0,0) ic.cobra_p2p(pin1=(0, 0, 0), pin2=(r*np.sin(np.pi*a/180), r*(1-np.cos(np.pi*a/180)), a), width1=w1, width2=w2, radius1=r, radius2=r).put(0,0) nd.export_gds()
Of course when I refer to cobra, I mean either cobra itself or cobra_p2p.
Paul24 November 2019 at 12:20 #5852
There are infinite ways to draw curves (waveguides) between two points. The cobra_p2p is one that specifically does not use a constant bend radius but smoothly goes from a straight waveguide to a curved waveguide and finds an optimal path according to some strategy, as described inside the function.
Hence, the Viper is a good approach to do what you want, but it is not yet an Interconnect. A proper Nazca Interconnect set up has a number of levels to make it work as effortlessly as possible when utilizing it as a designer. It uses a xsection and stores a number of properties that are reused when creating mask_elements, like strt, bend or cobra_p2p.
The following structure gives a short overview on what an Interconnect definition is built upon:
– add layers and their growth values
– add width, radius, etc.
B- mask_element template function:
– provide a xsection (e.g. get from Interconnect (C))
– provide default values (e.g. get from Interconnect (C))
– mask_element function:
– – the mask_element is a “closure”, i.e. the function and its environment are saved when creating it.
– – loop over all xsection layers:
– – – apply grow
– – – create and discretize the polygon based on the layer
– – add pins and other properties
C- Interconnect (class):
– add a xsection, see (A)
– add default values (get from xsection where possible)
– Interconnect methods:
– – create an interconnect cell from a mask_element cell (B) and neither cell will be instantiated (by choice).
– – add pins to the interconnect cell
Note that mask_elements exist without Interconnects. In other words, an Interconnect object is just a wrapper around mask_elements to group these elements together inside a single technology definition.
If we now turn to the Viper we find that it creates a spine or path [x(t), y(t)]. From that it creates a polygon by adding a width w(t) to the spine. The viper still needs proper pin directions and polygon geometry at the begin and end points, proper discretization w.r.t. to the mask resolution, loop over all layers in a xsection, etc.
From the user perspective the layout code for a bent waveguide as described in your example simplifies to something as in the example below. It generates the layout shown after the code.
Note that the radius, and width1 and width2, are nicely incorporated as keywords for the tapered_bend() to make it possible overrule defaults.
tapered_bend().put(0) ic.strt().put() tapered_bend(angle=90, width2=20, N=1000).put() tapered_bend(angle=-300, radius=60, width1=20, width2=0.5, N=2000).put() nd.export_gds()
The complete code to set it up is shown below. The viper functions x, y, w can be adapted to more or less any sensible set of functions for drawing a waveguide. The example demonstrates part A and B in the above explained structure. Putting it in Interconnect C is straight forward after this step and omitted in this example.
Note that the keywords “anglei” and “angleo” in viper() require Nazca-0.5.8 or up. You can run the code without them in earlier Nazca versions (with the viper) and notice that the viper polygons then do not connect “good enough” in case of a curvature in the begin and/or end point, because the polygon angles are no longer matching the pins their “exact” angle values in those points.
A next step, not in this example, could be to calculate the minimum N for a specific maximum resolution. Here you can take N “large enough”. If this function is called from an Interconnect object you would not use
nd.get_xsection(xs).widthbut get the Interconnect’s settings.
import numpy as np import math as m from functools import partial import nazca as nd from nazca.interconnects import Interconnect def Tp_tapered_bend(x, y, w, radius=100.0, angle=10.0, width1=None, width2=None, xs=None, layer=None, N=200): """Template for a specific Viper implementation. args: x (function): function in at least t, t in [0,1] y (function): function in at least t, t in [0,1] w (function): function in at least t, t in [0,1] Returns: function: Cell generating function """ def tapered_bend(radius=radius, angle=angle, width1=None, width2=None, xs=xs, layer=layer, N=N): """Specific Viper implementation. Returns: Cell: Cell based on a Viper """ N = N # discretization steps should be "large enough" for mask resolution name = 'viper_bend' # housekeeping for default values (add as needed): if width1 is None: width1 = nd.get_xsection(xs).width if width2 is None: width2 = nd.get_xsection(xs).width # Fill in all x, y, w function parameters except t: X = partial(x, radius=radius, angle=angle) Y = partial(y, radius=radius, angle=angle) W = partial(w, width1=width1, width2=width2) # Store begin and end points of the viper. # Note-1: begin and end angle of the viper spine should be taken # infinitesimally close to the begin and end. # Note-2: the polygon's begin and end edge should be perpendicular # to the local angles from Note-1 xa, ya = X(0), Y(0) xb, yb = X(1), Y(1) d = 1e-8 aa = m.degrees(m.atan2( Y(0)-Y(d), X(0)-X(d))) ab = m.degrees(m.atan2( Y(1)-Y(1-d), X(1)-X(1-d))) AB = (ab-aa-90) if AB < -180: AB += 180 # create the Cell: with nd.Cell(name=name, cnt=True) as C: for lay, grow, acc, line in nd.layeriter(xs, layer): (a1, b1), (a2, b2), c1, c2 = grow xygon = nd.util.viper( X, Y, lambda t: W(t) + b1-b2, # add the layer growth N=N, anglei = -aa, # remove for <0.5.8 angleo = AB # remove for <0.5.8 ) nd.Polygon(points=xygon, layer=lay).put(0) nd.Pin(name='a0', type=0, width=width1, xs=xs, show=True).put(xa, ya, aa) nd.Pin(name='b0', type=1, width=width2, xs=xs, show=True).put(xb, yb, ab) nd.put_stub(length=0) return C return tapered_bend if __name__ == "__main__": # add a technology: XS = nd.add_xsection('xs') XS.width = 1.0 XS.radius = 100.0 nd.add_layer2xsection(xsection='xs', layer=1) nd.add_layer2xsection(xsection='xs', layer=2, growx=4.0) ic = Interconnect(xs='xs') # create a specific viper-based mask_element: def x(t, radius, angle): return radius * np.cos(t * angle/180 * np.pi) def y(t, radius, angle): return radius * np.sin(t * angle/180 * np.pi) def w(t, width1=None, width2=None): return width1 + (width2 - width1) * t tapered_bend = Tp_tapered_bend(x, y, w, xs=ic.xs) # put waveguides: tapered_bend().put(0) ic.strt().put() tapered_bend(angle=90, width2=20, N=1000).put() tapered_bend(angle=-300, radius=60, width1=20, width2=0.5, N=2000).put() nd.export_gds()
Ronald25 November 2019 at 16:04 #5853paulMember
Thanks for the detailed answer. This is exactly the sort of function I was after and I think that this post will be helpful when it comes to creating custom Interconnect shapes.
With 0.5.4 this line
for lay, grow, acc, line in nd.layeriter(xs, layer):
returned a ValueError: not enough values to unpack (expected 4, got 3), so I commented the
lineparameter out since it wasn’t being used anywhere in the loop.
Paul25 November 2019 at 16:22 #5854
The “line” variable yielded by layeriter was added in Nazca-0.5.7, based on among others a request for a polyline option in a xsection rather than polygons: creating paths. See also xsections and layersfor the polyline usage.
For Nazca<0.5.7, indeed use
for lay, grow, acc in nd.layeriter(xs, layer):to loop over all layers in a xsection, as you did. Note that the Viper would need a minor upgrade for “edge” defined layers, rather than ‘width’ defined layers, but that is not relevant in this example.
Note that 0.5.8 needs to be released at the time of writing of this post, so you have to remark out anglei and angleo for now.
Ronald8 December 2019 at 22:40 #5898
- You must be logged in to reply to this topic.