References

GDS allows defining the component once in memory and reference to that structure in other components.

image0

As you build complex circuit you will make circuits that combine reference to other simpler circuits. Adding a reference is like having a pointer to the other devices.

The GDSII specification allows the use of references, and similarly gdsfactory uses them (with the add_ref() function). So what is a reference? Simply put: A reference does not contain any geometry. It only *points* to an existing geometry.

Say Alice has a ridiculously large polygon with 100 billion vertices in it that we’ll call BigPolygon. It’s huge, and she needs to use it in her design 250 times. Well, a single copy of BigPolygon takes up 100GB of memory just by itself, so she doesn’t want to make 250 copies of it. Since Alice is clever, she instead references the polygon 250 times. Each reference only uses a few bytes of memory – it only needs to know the memory address of BigPolygon and a few other things. In this way, she can keep one copy of BigPolygon and use it again and again wherever she needs to.

Let’s start by making a blank geometry (Component) then adding a single polygon to it.

[1]:
import numpy as np
import gdsfactory as gf

gf.config.set_plot_options(show_subports=False)

# Create a blank Component
p = gf.Component()

# Add a polygon
xpts = [0, 0, 5, 6, 9, 12]
ypts = [0, 1, 1, 2, 2, 0]
p.add_polygon([xpts, ypts])

# Quickplot the Component with the polygon in it
p
2021-09-25 16:18:39.068 | INFO     | gdsfactory.config:<module>:51 - 3.2.9
../_images/notebooks_01_references_2_1.png
[1]:
Unnamed_42af6f95: uid 1, ports [], aliases [], 1 polygons, 0 references

Now, pretend we’re in the same position as Alice: We want to reuse this polygon repeatedly but do not want to make multiple copies of it. To do so, we need to make a second blank Component, this time called D. In this new Component we’ll reference our Component P which contains our polygon.

[2]:
c = gf.Component()  # Create a new blank Component
poly_ref = c.add_ref(p)  # Reference the Component "p" that has the polygon in it
c  # Quickplot the reference-containing Component "c"
../_images/notebooks_01_references_4_0.png
[2]:
Unnamed_982cbfc5: uid 2, ports [], aliases [], 0 polygons, 1 references

OK, well that seemed to work, but it also seems thoroughly useless! It looks like you just made a copy of your polygon – but remember, you didn’t actually make a second polygon, you just made a reference (aka pointer) to the original polygon. Let’s add two more references to c:

[3]:
poly_ref2 = c.add_ref(p)  # Reference the Component "p" that has the polygon in it
poly_ref3 = c.add_ref(p)  # Reference the Component "p" that has the polygon in it

c  # Quickplot the reference-containing Component "c"
../_images/notebooks_01_references_6_0.png
[3]:
Unnamed_982cbfc5: uid 2, ports [], aliases [], 0 polygons, 3 references

Now you have 3x polygons all on top of each other. Again, this would appear useless, except that you can manipulate each reference indepedently. Notice that when you called c.add_ref(p) above, we saved the result to a new variable each time (poly_ref, poly_ref2, and poly_ref3)? You can use those variables to reposition the references.

[4]:
poly_ref2.rotate(15)  # Rotate the 2nd reference we made 15 degrees
poly_ref3.rotate(30)  # Rotate the 3rd reference we made 30 degrees
c
../_images/notebooks_01_references_8_0.png
[4]:
Unnamed_982cbfc5: uid 2, ports [], aliases [], 0 polygons, 3 references

Now you’re getting somewhere! You’ve only had to make the polygon once, but you’re able to reuse it as many times as you want.

Modifying the referenced geometry

What happens when you change the original geometry that the reference points to? In your case, your references in c all point to the Component p that with the original polygon. Let’s try adding a second polygon to p.

First you add the second polygon and make sure P looks like you expect:

[5]:
# Add a 2nd polygon to "p"
xpts = [14, 14, 16, 16]
ypts = [0, 2, 2, 0]
p.add_polygon([xpts, ypts], layer=(1, 0))
p
../_images/notebooks_01_references_10_0.png
[5]:
Unnamed_42af6f95: uid 1, ports [], aliases [], 2 polygons, 0 references

That looks good. Now let’s find out what happened to c that contains the three references. Keep in mind that you have not modified c or executed any functions/operations on c – all you have done is modify p.

[6]:
c
../_images/notebooks_01_references_12_0.png
[6]:
Unnamed_982cbfc5: uid 2, ports [], aliases [], 0 polygons, 3 references

When you modify the original geometry, all of the references automatically reflect the modifications. This is very powerful, because you can use this to make very complicated designs from relatively simple elements in a computation- and memory-efficienct way.

Let’s try making references a level deeper by referencing c. Note here we use the << operator to add the references – this is just shorthand, and is exactly equivalent to using add_ref()

[7]:
c2 = gf.Component()  # Create a new blank Component
d_ref1 = c2.add_ref(c)  # Reference the Component "c" that 3 references in it
d_ref2 = c2 << c  # Use the "<<" operator to create a 2nd reference to c
d_ref3 = c2 << c  # Use the "<<" operator to create a 3rd reference to c

d_ref1.move([20, 0])
d_ref2.move([40, 0])

c2
../_images/notebooks_01_references_14_0.png
[7]:
Unnamed_ab8e9ebe: uid 3, ports [], aliases [], 0 polygons, 3 references

As you’ve seen you have two ways to add a reference to our component:

  1. create the reference and add it to the component

[8]:
c = gf.Component("reference_sample")
w = gf.components.straight(width=0.6)
wr = w.ref()
c.add(wr)
c
../_images/notebooks_01_references_16_0.png
[8]:
reference_sample: uid 4, ports [], aliases [], 0 polygons, 1 references
  1. or do it in a single line

[9]:
c = gf.Component("reference_sample_shorter_syntax")
wr = c << gf.components.straight(width=0.6)
c
../_images/notebooks_01_references_18_0.png
[9]:
reference_sample_shorter_syntax: uid 6, ports [], aliases [], 0 polygons, 1 references

in both cases you can move the reference wr after created

[10]:
import gdsfactory as gf

c = gf.Component("two_references")
wr1 = c << gf.components.straight(width=0.6)
wr2 = c << gf.components.straight(width=0.6)
wr2.movey(10)
c.add_ports(wr1.get_ports_list(), prefix="top_")
c.add_ports(wr2.get_ports_list(), prefix="bot_")
[11]:
c.ports
[11]:
{'top_o1': Port (name top_o1, midpoint [0. 0.], width 0.6, orientation 180.0, layer (1, 0), port_type optical),
 'top_o2': Port (name top_o2, midpoint [10.  0.], width 0.6, orientation 0.0, layer (1, 0), port_type optical),
 'bot_o1': Port (name bot_o1, midpoint [ 0. 10.], width 0.6, orientation 180.0, layer (1, 0), port_type optical),
 'bot_o2': Port (name bot_o2, midpoint [10. 10.], width 0.6, orientation 0.0, layer (1, 0), port_type optical)}

You can also auto_rename ports using gdsfactory default convention, where ports are numbered clockwise starting from the bottom left

[12]:
c.auto_rename_ports()
[13]:
c.ports
[13]:
{'o1': Port (name o1, midpoint [0. 0.], width 0.6, orientation 180.0, layer (1, 0), port_type optical),
 'o4': Port (name o4, midpoint [10.  0.], width 0.6, orientation 0.0, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [ 0. 10.], width 0.6, orientation 180.0, layer (1, 0), port_type optical),
 'o3': Port (name o3, midpoint [10. 10.], width 0.6, orientation 0.0, layer (1, 0), port_type optical)}
[14]:
c
../_images/notebooks_01_references_25_0.png
[14]:
two_references: uid 8, ports ['o1', 'o4', 'o2', 'o3'], aliases [], 0 polygons, 2 references

Arrays of references

In GDS, there’s a type of structure called a “CellArray” which takes a cell and repeats it NxM times on a fixed grid spacing. For convenience, Component includes this functionality with the add_array() function. Note that CellArrays are not compatible with ports (since there is no way to access/modify individual elements in a GDS cellarray)

gdsfactory also provides with more flexible arrangement options if desired, see for example grid() and packer().

As well as gf.components.array and gf.components.array_2d

Let’s make a new Component and put a big array of our Component c in it:

[15]:
c3 = gf.Component()  # Create a new blank Component
aref = c3.add_array(
    c, columns=6, rows=3, spacing=[20, 15]
)  # Reference the Component "c" 3 references in it with a 3 rows, 6 columns array
c3
../_images/notebooks_01_references_27_0.png
[15]:
Unnamed_34de04f5: uid 10, ports [], aliases [], 0 polygons, 1 references

CellArrays don’t have ports and there is no way to access/modify individual elements in a GDS cellarray.

gdsfactory provides you with similar functions in gf.components.array and gf.components.array_2d

[16]:
c4 = gf.Component()  # Create a new blank Component
aref = c4 << gf.components.array(component=c, columns=3, rows=2)
c4.add_ports(aref.get_ports_list())
c4
../_images/notebooks_01_references_29_0.png
[16]:
Unnamed_d10e76f3: uid 11, ports ['o1_1_1', 'o1_1_2', 'o1_1_3', 'o2_1_1', 'o2_1_2', 'o2_1_3', 'o1_2_1', 'o1_2_2', 'o1_2_3', 'o2_2_1', 'o2_2_2', 'o2_2_3', 'o3_2_1', 'o3_2_2', 'o3_2_3', 'o4_2_1', 'o4_2_2', 'o4_2_3', 'o3_1_1', 'o3_1_2', 'o3_1_3', 'o4_1_1', 'o4_1_2', 'o4_1_3'], aliases [], 0 polygons, 1 references
[17]:
gf.c.array?

You can also create an array of references for periodic structures. Lets create a Distributed Bragg Reflector

[18]:
import gdsfactory as gf


@gf.cell
def dbr_cell(w1=0.5, w2=0.6, l1=0.2, l2=0.4, straight_function=gf.components.straight):
    c = gf.Component()
    c1 = c << straight_function(length=l1, width=w1)
    c2 = c << straight_function(length=l2, width=w2)
    c2.connect(port="o1", destination=c1.ports["o2"])
    c.add_port("o1", port=c1.ports["o1"])
    c.add_port("o2", port=c2.ports["o2"])
    return c


w1 = 0.5
w2 = 0.6
l1 = 0.2
l2 = 0.4
n = 3
straight_function = gf.components.straight
c = gf.Component("reference_array")
cell = dbr_cell(w1=w1, w2=w2, l1=l1, l2=l2, straight_function=straight_function)
cell
../_images/notebooks_01_references_32_0.png
[18]:
dbr_cell_l1200n_l2400n__bfa30883: uid 14, ports ['o1', 'o2'], aliases [], 0 polygons, 2 references
[19]:
c = gf.Component("DBR")
c.add_array(cell, columns=n, rows=1, spacing=(l1 + l2, 100))
c
../_images/notebooks_01_references_33_0.png
[19]:
DBR: uid 17, ports [], aliases [], 0 polygons, 1 references

Finally we need to add ports to the new component

[20]:
p0 = c.add_port("o1", port=cell.ports["o1"])
p1 = c.add_port("o2", port=cell.ports["o2"])

p1.midpoint = [(l1 + l2) * n, 0]
c
../_images/notebooks_01_references_35_0.png
[20]:
DBR: uid 17, ports ['o1', 'o2'], aliases [], 0 polygons, 1 references

Connect references

We have seen that once you create a reference you can manipulate the reference to move it to a location. Here we are going to connect that reference to a port. Remeber that we follow that a certain reference source connects to a destination port

[21]:
gf.components.bend_circular()
../_images/notebooks_01_references_37_0.png
[21]:
bend_circular: uid 18, ports ['o1', 'o2'], aliases [], 1 polygons, 0 references
[22]:
c = gf.Component("sample_reference_connect")

mmi = c << gf.components.mmi1x2()
b = c << gf.components.bend_circular()
b.connect("o1", destination=mmi.ports["o2"])

c.add_port("o1", port=mmi.ports["o1"])
c.add_port("o2", port=b.ports["o2"])
c.add_port("o3", port=mmi.ports["o3"])
c
../_images/notebooks_01_references_38_0.png
[22]:
sample_reference_connect: uid 19, ports ['o1', 'o2', 'o3'], aliases [], 0 polygons, 2 references

Port naming

You have the freedom to name the ports as you want, and you can use gf.port.auto_rename_ports(prefix='o') to rename them later on.

Here is the default naming convention.

Ports are numbered clock-wise starting from the bottom left corner

Optical ports have o prefix and Electrical ports e prefix

The port naming comes in most cases from the gdsfactory.cross_section. For example

  • gdsfactory.cross_section.strip has ports o1 for input and o2 for output

  • gdsfactory.cross_section.metal1 has ports e1 for input and e2 for output

[23]:
import gdsfactory as gf

size = 4
c = gf.components.nxn(west=2, south=2, north=2, east=2, xsize=size, ysize=size)
c
../_images/notebooks_01_references_40_0.png
[23]:
nxn_east2_north2_south2_a2bd1ff0: uid 24, ports ['o1', 'o2', 'o6', 'o5', 'o3', 'o4', 'o8', 'o7'], aliases [], 0 polygons, 1 references
[24]:
c = gf.c.straight_heater_metal(length=30)
c
../_images/notebooks_01_references_41_0.png
[24]:
straight_heater_metal_u_c4123e83: uid 29, ports ['o1', 'o2', 'e1', 'e2'], aliases [], 0 polygons, 3 references
[25]:
c.ports
[25]:
{'o1': Port (name o1, midpoint [0. 0.], width 0.5, orientation 180.0, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [30.  0.], width 0.5, orientation 0.0, layer (1, 0), port_type optical),
 'e1': Port (name e1, midpoint [-5.5  0. ], width 11.0, orientation 180, layer (49, 0), port_type electrical),
 'e2': Port (name e2, midpoint [35.5  0. ], width 11.0, orientation 0, layer (49, 0), port_type electrical)}

You can get the optical ports by layer

[26]:
c.get_ports_dict(layer=(1, 0))
[26]:
{'o1': Port (name o1, midpoint [0. 0.], width 0.5, orientation 180.0, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [30.  0.], width 0.5, orientation 0.0, layer (1, 0), port_type optical)}

or by width

[27]:
c.get_ports_dict(width=0.5)
[27]:
{'o1': Port (name o1, midpoint [0. 0.], width 0.5, orientation 180.0, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [30.  0.], width 0.5, orientation 0.0, layer (1, 0), port_type optical)}
[28]:
c = gf.c.straight_heater_metal()
c.auto_rename_ports_layer_orientation()
c.ports
[28]:
{'1_0_W0': Port (name 49_0_W1, midpoint [0. 0.], width 0.5, orientation 180.0, layer (1, 0), port_type optical),
 '1_0_E0': Port (name 49_0_E0, midpoint [320.   0.], width 0.5, orientation 0.0, layer (1, 0), port_type optical),
 '49_0_W0': Port (name 49_0_W0, midpoint [-5.5  0. ], width 11.0, orientation 180, layer (49, 0), port_type electrical),
 '49_0_E1': Port (name 49_0_E1, midpoint [325.5   0. ], width 11.0, orientation 0, layer (49, 0), port_type electrical)}
[29]:
c.auto_rename_ports()
c.ports
[29]:
{'o1': Port (name o1, midpoint [0. 0.], width 0.5, orientation 180.0, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [320.   0.], width 0.5, orientation 0.0, layer (1, 0), port_type optical),
 'e1': Port (name e1, midpoint [-5.5  0. ], width 11.0, orientation 180, layer (49, 0), port_type electrical),
 'e2': Port (name e2, midpoint [325.5   0. ], width 11.0, orientation 0, layer (49, 0), port_type electrical)}

You can also rename them with a different port naming convention

  • prefix: add e for electrical o for optical

  • clockwise

  • counter-clockwise

  • orientation E East, W West, N North, S South

Here is the default one we use (clockwise starting from bottom left west facing port)

    3   4
    |___|_
2 -|      |- 5
   |      |
1 -|______|- 6
    |   |
    8   7
[30]:
import gdsfactory as gf

c = gf.Component()
nxn = gf.c.nxn(west=2, north=2, east=2, south=2)
ref = c.add_ref(nxn)
c.add_ports(ref.ports)
c
../_images/notebooks_01_references_50_0.png
[30]:
Unnamed_6ff958f2: uid 54, ports ['o1', 'o2', 'o6', 'o5', 'o3', 'o4', 'o8', 'o7'], aliases [], 0 polygons, 1 references
[31]:
ref.get_ports_list()  # by default returns ports clockwise starting from bottom left west facing port
[31]:
[Port (name o1, midpoint [0.   1.25], width 0.5, orientation 180, layer (1, 0), port_type optical),
 Port (name o2, midpoint [0.   6.75], width 0.5, orientation 180, layer (1, 0), port_type optical),
 Port (name o3, midpoint [1.25 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 Port (name o4, midpoint [6.75 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 Port (name o5, midpoint [8.   6.75], width 0.5, orientation 0, layer (1, 0), port_type optical),
 Port (name o6, midpoint [8.   1.25], width 0.5, orientation 0, layer (1, 0), port_type optical),
 Port (name o7, midpoint [6.75 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical),
 Port (name o8, midpoint [1.25 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical)]

You can also get the ports counter-clockwise

    4   3
    |___|_
5 -|      |- 2
   |      |
6 -|______|- 1
    |   |
    7   8
[32]:
ref.get_ports_list(clockwise=False)
[32]:
[Port (name o6, midpoint [8.   1.25], width 0.5, orientation 0, layer (1, 0), port_type optical),
 Port (name o5, midpoint [8.   6.75], width 0.5, orientation 0, layer (1, 0), port_type optical),
 Port (name o4, midpoint [6.75 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 Port (name o3, midpoint [1.25 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 Port (name o2, midpoint [0.   6.75], width 0.5, orientation 180, layer (1, 0), port_type optical),
 Port (name o1, midpoint [0.   1.25], width 0.5, orientation 180, layer (1, 0), port_type optical),
 Port (name o8, midpoint [1.25 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical),
 Port (name o7, midpoint [6.75 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical)]
[33]:
ref.ports_layer
[33]:
{'1_0_W0': 'o1',
 '1_0_W1': 'o2',
 '1_0_E0': 'o6',
 '1_0_E1': 'o5',
 '1_0_N0': 'o3',
 '1_0_N1': 'o4',
 '1_0_S0': 'o8',
 '1_0_S1': 'o7'}
[34]:
ref.port_by_orientation_cw("W0")
[34]:
Port (name o1, midpoint [0.   1.25], width 0.5, orientation 180, layer (1, 0), port_type optical)
[35]:
ref.port_by_orientation_ccw("W1")
[35]:
Port (name o1, midpoint [0.   1.25], width 0.5, orientation 180, layer (1, 0), port_type optical)
[36]:
c = gf.Component()
nxn = gf.c.nxn(west=2, north=2, east=2, south=2)
ref = c.add_ref(nxn)
ref.rotate(-90)
c.add_ports(ref.ports)
c
../_images/notebooks_01_references_57_0.png
[36]:
Unnamed_6ac56cda: uid 57, ports ['o1', 'o2', 'o6', 'o5', 'o3', 'o4', 'o8', 'o7'], aliases [], 0 polygons, 1 references
[37]:
ref.ports_layer
[37]:
{'1_0_N0': 'o1',
 '1_0_N1': 'o2',
 '1_0_S0': 'o6',
 '1_0_S1': 'o5',
 '1_0_E1': 'o3',
 '1_0_E0': 'o4',
 '1_0_W1': 'o8',
 '1_0_W0': 'o7'}
[38]:
ref.port_by_orientation_ccw("W0")
[38]:
Port (name o8, midpoint [-2.29621275e-16 -1.25000000e+00], width 0.5, orientation 180, layer (1, 0), port_type optical)
[39]:
ref.port_by_orientation_ccw("W1")
[39]:
Port (name o7, midpoint [-1.23995488e-15 -6.75000000e+00], width 0.5, orientation 180, layer (1, 0), port_type optical)

You can still reference the port by their orientation.

For example, lets extend the top East facing port

[40]:
import gdsfactory as gf

nxn = gf.c.nxn(west=2, north=2, east=2, south=2)
c = gf.c.extension.extend_ports(component=nxn, port_names=[nxn.ports_layer["1_0_W0"]])
c
../_images/notebooks_01_references_62_0.png
[40]:
extend_ports_componentn_29852e11: uid 62, ports ['o1', 'o2', 'o3', 'o4', 'o5', 'o6', 'o7', 'o8'], aliases [], 1 polygons, 1 references
[41]:
c.ports
[41]:
{'o1': Port (name o1, midpoint [-5.    1.25], width 0.5, orientation 180, layer (1, 0), port_type optical),
 'o2': Port (name o2, midpoint [0.   6.75], width 0.5, orientation 180, layer (1, 0), port_type optical),
 'o3': Port (name o3, midpoint [1.25 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 'o4': Port (name o4, midpoint [6.75 8.  ], width 0.5, orientation 90, layer (1, 0), port_type optical),
 'o5': Port (name o5, midpoint [8.   6.75], width 0.5, orientation 0, layer (1, 0), port_type optical),
 'o6': Port (name o6, midpoint [8.   1.25], width 0.5, orientation 0, layer (1, 0), port_type optical),
 'o7': Port (name o7, midpoint [6.75 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical),
 'o8': Port (name o8, midpoint [1.25 0.  ], width 0.5, orientation 270, layer (1, 0), port_type optical)}

component_sequence

When you have repetitive connections you can describe the connectivity as an ASCII map

[42]:
import gdsfactory as gf
[43]:
bend180 = gf.components.bend_circular180()
wg_pin = gf.components.straight_pin(length=40)
wg = gf.components.straight()

# Define a map between symbols and (component, input port, output port)
symbol_to_component = {
    "D": (bend180, "o1", "o2"),
    "C": (bend180, "o2", "o1"),
    "P": (wg_pin, "o1", "o2"),
    "-": (wg, "o1", "o2"),
}

# Generate a sequence
# This is simply a chain of characters. Each of them represents a component
# with a given input and and a given output

sequence = "DC-P-P-P-P-CD"
component = gf.components.component_sequence(
    sequence=sequence, symbol_to_component=symbol_to_component
)
component.name = "component_sequence"
component
../_images/notebooks_01_references_66_0.png
[43]:
component_sequence: uid 84, ports ['o1', 'o2'], aliases ['D1', 'C1', '-1', 'P1', '-2', 'P2', '-3', 'P3', '-4', 'P4', '-5', 'C2', 'D2'], 0 polygons, 13 references

As the sequence is defined as a string you can use the string operations to build complicated sequences

[ ]: