Forum Replies Created
-
AuthorPosts
-
Ronald
KeymasterDear Miakatt,
There is a discussion related to polygon inversions in this postand on growing polygons in this post. Personally, I consider trenching as you describe more a mask post-processing step and I would keep/define both polygon layers at first and do the logical operation at the end. Even though Nazca has its own gds manipulation, at the moment it does not (have to) dive too deep into the specifics of polygon boolean operations as this step can be handled very well by Kayout as a post-process without loosing any design efficiency or inconvenience. Your flow may differ though.
When checking out the PICwriter tool you mentioned, I see it uses gdspy. I guess gdspy handles the polygon inversion in that case. I haven’t used gdspy but I guess you can send the polygons to it, e.g. from Nazca, and get the inversion back. This method should most certainly be doable utilizing Klayout, which can be imported as a Python module. A little code wrapper can make that syntax Nazca conform so you do not have to deal with two interfaces.
Ronald
Ronald
KeymasterDear Douglas,
The viper has been integrated as a mask element in nazca 0.5.8.
This tutorial may be of use: Free form curves
Ronald
Ronald
KeymasterDear Paul,
For more information on the viper integration as a mask element this tutorial may be helpful: Free form curves
Ronald
Ronald
KeymasterDear Paul,
There is a special mechanism in Nazca to replace parametric and/or static cells.
In case of static cells the example below shows how you can use on or more GDS library files as a source to replace cells in your original gds by name. It doesn’t matter how many or how deep in the tree those cells are:import nazca as nd # File name of gds input. gdsin = 'example.gds' # a list of scell (static cell) libraries: # and a cellmap for each library file: { : } # where resides in gdsin and in the lib. scell_libs = { 'scell_lib1.gds': { 'C': 'newC', # other cell name mappings }, # other libs and mappings as needed } # Replace cells and write output to a new gds file (gdsin will not be changed). nd.replaceCells( gdsin=gdsin, ScellMapping=scell_libs )Starting with the following cell structure
A B C Dthe replacement C -> newC gives
A B newC DAlthough A has different content now, effectively only its references to C have been changed. So, I kept A’s original name.
In contrast, if you replace A -> newA you get
newA # branches in newA DIf you replace A by newA the substructure (the tree) of A is irrelevant as it is replaced as a whole; You remove the whole branch behind the cut. Cell newA can be a cell or tree of cells.
If newA would look like
newA Cthen the replacement result A -> newA is
newA C DIf newA would look like
newA newCthen the replacement result is
newA newC DIf you have two libraries like
# lib1.gds: NewA C # lib2.gds: newCthen you would use
gdsin = 'example.gds' scell_libs = { 'lib1.gds': {'A': 'newA'}, 'lib2.gds': {'C': 'newC'} } nd.replaceCells( gdsin=gdsin, ScellMapping=scell_libs )to obtain
newA newC DNote: Since Python 3.7 dictionaries are ordered as a language feature, which is important as replacement is not commutative in the order of the libraries (in ‘scell_libs’ above). In earlier Python versions you would make consecutive calls of nd.replaceCells() with one library at a time to ensure the library order.
Ronald
Ronald
KeymasterDear Paul,
As an example on how to add a cell to a gds I take a small detour. First load the gds as a “library” in Nazca:
LIB = nd.load_gds(asdict=True)Due to asdict=True the load_gds() returns a dictionary where the keys are the cell names and the values are the Cells. By default only top cells are expressed as the keys in dict LIB, but this can be changed to include all cells as keys:
LIB = nd.load_gds(filename=, asdict=True, topcellsonly=FalseHence, if you have a gds “test.gds” with top cells named “A” and “B”, you can load and place them like this:
LIB = nd.load_gds(filename="test.gds", asdict=True) LIB['A'].put() LIB['B'].put()The default pin of the library cells (here LIB[‘A’], LIB[‘B’]) is set to the ‘org’ pin.
You can load and export the original gds in the following way:
LIB = nd.load_gds(filename=, asdict=True) cell_list = list(LIB.values()) nd.export_gds(cell_list)You can add extra cells to cell_list before calling nd.export_gds.
Does that cover your use case?Ronald
Ronald
KeymasterDear Paul,
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.
Ronald
Ronald
KeymasterDear Paul,
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:
A- xsection:
– 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 propertiesC- 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 cellNote 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()Ronald
Ronald
KeymasterDear Paul,
The put statement takes (x, y, a), with x, y in um and a in degrees. You can use this as follows (and omit the rotate keyword):
cell_A.put(0, 0, 90)Some more background. The put() method is one of the few cases where you should only provide values as first entries, and avoid a keyword assignment like x=1, because put() interprets the first 5 positional entries to decide what to do. In addition, there is a set of keywords available like “array” for gds arrays, “scale” for scaling, and “flip” and “flop” for mirroring. These keywords translate into a cell property in gds and you will see their values back when double clicking a cell in e.g. KLayout. See also Cell.put().
Ronald
Ronald
KeymasterRonald
KeymasterDear Joe,
1. Cell creation and placing cells (the Nazca “put” method) in a layout are fundamentally two separate things and actually a core Nazca working principle. You can create a complete layout in a cell hierarchy, and as long as you do not instantiate the root cell of that hierarchy in a cell that is exported, it won’t be part of the exported layout. You can access all that cell’s information though and everything is calculated inside that root cell without ever putting it.
The cell renaming warning can always be resolved. Either by using the @hashme decorator, see https://nazca-design.org/hashme/ , or by assigning a cell to a variable instead of regenerating it multiple times in a function call.
Another way to reuse the cell name is to delete it from the cellnames dictionary, which also gets rid of the cell itself (via Python garbage collection if you don’t keep a reference to an object). I think that what you describe you want to do is to generate a cell definition multiple times with the same name, each time read something from it and delete it. Something like this?:
import nazca as nd def give_me_a_cell(i): with nd.Cell('name') as C: # stuff in the cell pass return C for i in range(100): A = give_me_a_cell(i=i) # read out stuff from cell A del nd.cfg.cellnames['name']2.
nd.Cell(instantiate=False)makes instances of this cell resolve in their parent(s) upon export. Interconnects, mask elements (like nd.strt) and stubs have instantiate=False by default. New user cells have instantiate=True by default.Ronald
Ronald
KeymasterDear Daniel,
Indeed, the interconnects were a step too high up (they return lists of connecting elements) and true that you didn’t argue for polylines as polygon replacement. See it as general background information in the discussion as this topic pops up now and then.
I had a look a few days ago into the polylines when defining interconnects, and actually I implemented the polyline already as an option when adding a layer to a xsection (setting polyline=True) for some elements (strt, bend, taper). The execution is mainly in mask_elements.py. It will be available in Nazca-0.5.6. Having polylines in a xsection can be useful in some EBPG processes. There is a catch though: The interconnects generate a set of connecting polylines, not a single long polyline. Not sure if that would ruin the party for your use case. If you could share how you use polylines that may help defining a better implementation if needed.
Ronald
Ronald
KeymasterDear mbjvrijn,
The shape in your second code sample
can be reproduced using the relations for a and b in my previous post. In the code below I took y1 and y2 from the output of your code. The centre line of the interconnect lies at y=0.
import nazca as nd # get (a, b) for the left and right edge: def get_edge(y1, y2): w1 = 1.0 # w1 and w2 can be chosen arbitrary, but not the same w2 = 2.0 a = (y1 - y2) / (w1 - w2) b = (y2*w1 - y1*w2) / (w1 - w2) return a, b al, bl = get_edge(y1=1.5, y2=30) ar, br = get_edge(y1=-1.5, y2=10) # The actual xsection definition: xs = nd.add_xsection(name='metal_strt') nd.add_layer(name='L1', layer=1) nd.add_layer2xsection(xsection='metal_strt', leftedge=(al, bl), rightedge=(ar, br), layer='L1') ic = nd.interconnects.Interconnect(xs='metal_strt', width=1.0, radius=40) # reuse w1 value from (a, b) calc. ic.strt(10).put(0) ic.taper(20, width2=2.0).put() # reuse w2 value from the (a, b) calculation ic.strt(10, width=2.0).put() nd.export_plt()Ronald
Ronald
KeymasterDear mjbvrijn,
As of Nazca 0.5.4 a waveguide in a specific layer inside a xsection can be defined by providing the left edge and right edge of the waveguide independently. Multiple of such guides can be defined inside a single xsection. The edge is define by y = a*w+b, as clarified in the picture below. Parameter w is the width setting of the xsection.
As long as you can describe an edge of by a single y = a*w +b statement, a standard inteconnect taper will work for a case as you describe, as shown in the next example. Note that a and b parameters for an edge are provided as tuples (a, b) via the keywords leftedge and rightedge:
import nazca as nd xs = nd.add_xsection(name='GSG') nd.add_layer(name='L1', layer=1) nd.add_layer2xsection(xsection='GSG', leftedge=(0.5, 0), rightedge=(-0.5, 0), layer='L1') nd.add_layer2xsection(xsection='GSG', leftedge=(3.5, 0), rightedge=(1.5, 0), layer='L1') nd.add_layer2xsection(xsection='GSG', leftedge=(-1.5, 0), rightedge=(-3.5, 0),layer='L1') tr = nd.interconnects.Interconnect(xs='GSG', width=3.0, radius=40) tr.strt(length=40).put(0) tr.taper(length=20, width2=6.0).put() tr.strt(length=40, width=6.0).put() nd.export_plt()If you start with the dimension on the left (y1) and right (y2) side of the taper in the above example you can find a and b of an edge as follows:
y1 = a1 * w1 + b1
y2 = a2 * w2 + b2
and solve for y1 =Â y2
This gives
a = (y1 – y2) / (w1 – w2)
b = (y2*w1 – y1*w2) / (w1 – w2)
and note that the widths w1 and w2 can be considered scaling numbers instead of actual widths of a guide in the xsection.
Ronald
Ronald
KeymasterDear Daniel,
Polygon discretization can be more accurately controlled than polylines. Also, the latter have to be “interpreted” leading to a less deterministic outcome in various readers than polygons. Another limitation for polylines is that they can not describe a change in width or an asymmetric waveguide. Hence, Interconnect objects in Nazca use polygons to describe waveguides (and e.g. metal lines).
There may be other situations where you want to use polylines. You still can use Interconnects.py methods to get to polylines. Polygons are only the final step in creating interconnects. Each interconnect has a “_solve” method that returns a polyline/path. Hence, call the _solve method to get a path = [(x1, y1), (x2, y2, …], which you then use as
nd.Polyline(points=path, width=2.0).put().Alternatively, it is quite straight forward to add a switch to mask_elements.py methods to produce a polyline rather than a polygon, with the above mentioned limitations of polylines.
Ronald
Ronald
KeymasterDear Daniel,
I may have over interpreted your question.
For a path (polyline) simply use the Polyline method:import nazca as nd nd.Polyline(points=[(0, 0), (10,0)], width=3.0, layer=2).put(0) nd.export_gds()Ronald
Ronald
KeymasterRonald
KeymasterTo add Xaveer’s answer, there are 5 class objects in Nazca with a put() method. Some of these objects have an equivalent in GDS and most of these put methods have return type None, including the Polygon. See the overview below.
The Pyclipper needs to work on polygon points as Xaveer explained. For the ‘grow’ this has also been implemented as a Polygon class method, which internally operates on the polygon stored in the Polygon object.
import nazca as nd pgon1 = nd.Polygon(points=[(0, 0), (5, 0), (0,5)]) pgon2 = pgon1.grow(grow=1) pgon1.put(0) pgon2.put(10) nd.export_plt()Ronald
Ronald
KeymasterDear Gozde,
You can use Nazca Polygon objects for that, which you feed with “normal” polygons [(x1, y1), (x2, y2), … ]. There is a helper module nazca.geometries with methods which generate polygons for various shapes to make life easier.
import nazca as nd import nazca.geometries as geom for i in range(10): points = geom.arc(radius=10*i, width=8-0.5*i, angle=40+10*i, N=50) nd.Polygon(points=points, layer=1).put(0) nd.export_plt()With some extra rotation:
for i in range(10): points = geom.arc(radius=10+10*i, width=8-0.5*i, angle=40+10*i, N=50) nd.Polygon(points=points, layer=1).put(0, 0, i*15) nd.export_plt()Ronald
Ronald
KeymasterDear Daneel,
You can apply a layermap dictionary upon export, like {old_layer: new_layer}:
import nazca as nd points = [(0, 0), (5, 0), (0, 5)] nd.Polygon(points, layer=1).put(0) nd.Polygon(points, layer=2).put(5) nd.export_gds(layermap={1:5}, layermapmode='none') #nd.export_gds(layermap={1:5, 2:None}) # layermapmode default = 'all'The layermapmode keyword sets if you start the mapping with all layers removed (‘none’) or all layers still in (‘all’). In the commented out export_gds line I noticed that layer 2 exports to Nazca’s “dump-layer” 1111/0 (Nazca-0.5.3). It is better to have None-mappings removed completely though and I will adapt that.
Ronald
Ronald
KeymasterDear estiak2126,
Vertical text could roughly mean two things. See this example:
import nazca as nd nd.text('vertical').put(0, 0, 90) nd.text('\n'.join('hotel')).put(50) nd.export_plt()Ronald
-
AuthorPosts