Geometry#

gdsfactory provides you with some geometric functions

Boolean / outline / offset / invert#

There are several common boolean-type operations available in the geometry library. These include typical boolean operations (and/or/not/xor), offsetting (expanding/shrinking polygons), outlining, and inverting.

Boolean#

The gf.geometry.boolean() function can perform AND/OR/NOT/XOR operations, and will return a new geometry with the result of that operation.

import gdsfactory as gf

E = gf.components.ellipse(radii=(10, 5), layer=(1, 0))
R = gf.components.rectangle(size=[15, 5], layer=(2, 0))
C = gf.geometry.boolean(A=E, B=R, operation="not", precision=1e-6, layer=(3, 0))
# Other operations include 'and', 'or', 'xor', or equivalently 'A-B', 'B-A', 'A+B'

# Plot the originals and the result
D = gf.Component("bool")
D.add_ref(E)
D.add_ref(R).movey(-1.5)
D.add_ref(C).movex(30)
D.plot()
../_images/7824ce08fe85b30a92bf041477d9a70acd86ca173e69e07178e6a2c2b0155ba7.png

To learn how booleans work you can try all the different operations not, and, or, xor

import gdsfactory as gf

operation = "not"
operation = "and"
operation = "or"
operation = "xor"

r1 = (8, 8)
r2 = (11, 4)
r1 = (80, 80)
r2 = (110, 40)

angle_resolution = 0.1

c1 = gf.components.ellipse(radii=r1, layer=(1, 0), angle_resolution=angle_resolution)
c2 = gf.components.ellipse(radii=r2, layer=(1, 0), angle_resolution=angle_resolution)
%time

c3 = gf.geometry.boolean_klayout(
    c1, c2, operation=operation, layer1=(1, 0), layer2=(1, 0), layer3=(1, 0)
)  # KLayout booleans
c3.plot()
CPU times: user 7 µs, sys: 2 µs, total: 9 µs
Wall time: 5.72 µs
2024-04-27 01:04:33.637 | INFO     | gdsfactory.component:_write_library:2003 - Wrote to '/tmp/gdsfactory/ellipse_4aa83906.gds'
2024-04-27 01:04:33.642 | INFO     | gdsfactory.component:_write_library:2003 - Wrote to '/tmp/gdsfactory/ellipse_cb56ffd1.gds'
../_images/17e5302d8c8c53f600a981daea27c151ee737539b1f86de092b066ab84881c2a.png
%time
c4 = gf.geometry.boolean(c1, c2, operation=operation)
c4.plot()
CPU times: user 6 µs, sys: 3 µs, total: 9 µs
Wall time: 5.72 µs
../_images/d15edd43eacf9ec990a8d6694bac3bbb5755f4f23299b38b9e830bd57a8cfc37.png

Offset#

The offset() function takes the polygons of the input geometry, combines them together, and expands/contracts them. The function returns polygons on a single layer and does not respect layers.

import gdsfactory as gf

# Create `T`, an ellipse and rectangle which will be offset (expanded / contracted)
T = gf.Component("ellipse_and_rectangle")
e = T << gf.components.ellipse(radii=(10, 5), layer=(1, 0))
r = T << gf.components.rectangle(size=[15, 5], layer=(2, 0))
r.move([3, -2.5])

Texpanded = gf.geometry.offset(T, distance=2, precision=1e-6, layer=(2, 0))
Texpanded.name = "expanded"
Tshrink = gf.geometry.offset(T, distance=-1.5, precision=1e-6, layer=(2, 0))
Tshrink.name = "shrink"

# Plot the original geometry, the expanded, and the shrunk versions
offsets = gf.Component("top")
t1 = offsets.add_ref(T)
t2 = offsets.add_ref(Texpanded)
t3 = offsets.add_ref(Tshrink)
offsets.distribute([t1, t2, t3], direction="x", spacing=5)
offsets.plot()
../_images/fc22f2b7aeeb42690799da52e2dd5c386b7565b7b872c0e1efc75cf9fb4558a3.png

gf.geometry.offset is also useful for remove acute angle DRC errors.

You can do a positive offset to grow the polygons followed by a negative offset.

c = gf.Component("demo_dataprep")
c1 = gf.components.coupler_ring(cross_section="xs_rc2", radius=20)
c1.plot()
../_images/cac8e9aa456a3d01a691386f7826aa564d66996206d4a17536470c8e60455e54.png
d = 0.8
c2 = gf.geometry.offset(c1, distance=+d, layer=(3, 0))
c3 = gf.geometry.offset(c2, distance=-d, layer=(3, 0))
c << c1
c << c3
c.plot()
../_images/1bd3bc1ca8cde22cb7a418cc3a30b584966a31bed018255bcdc5843217157dad.png

You can also run it as a function over a Component.

from functools import partial
from gdsfactory.geometry.maskprep import get_polygons_over_under, over_under

over_under_slab = partial(over_under, layers=((3, 0),), distances=(0.5,))

c1 = gf.components.coupler_ring(cross_section="xs_rc2", radius=20)
c2 = over_under_slab(c1)
c2.plot()
../_images/9e4227a483651b85a1cdc1cd587884e2bdd9afcdc24a0dcd07ac1a89fddb0b97.png

You can also add extra polygons on top

get_polygons_over_under_slab = partial(
    get_polygons_over_under, layers=[(3, 0)], distances=(0.5,)
)
ring = gf.components.coupler_ring(cross_section="xs_rc2", radius=20)
ring = over_under_slab(ring)

c = gf.Component("compnent_clean")
ref = c << ring
polygons = get_polygons_over_under_slab(ref)
c.add(polygons)
c.plot()
../_images/20a42702cc1da821c999396b51277312bdd0ef8274154d328f1ac3ce2a946d9f.png

The fix_underplot decorator performs a combination of offset, AND, and NOT to ensure minimum inclusions of shapes:

from gdsfactory.geometry.maskprep import fix_underplot

c1 = gf.Component("component_initial")
c1 << gf.components.rectangle(size=(4, 4), layer="WG")
c1 << gf.components.rectangle(size=(2, 2), layer="SLAB150")
slab = c1 << gf.components.rectangle(size=(2, 2), layer="SLAB90")
slab.move((3, 1))
c1.plot()
../_images/8aa37982c8b9b0e933549eed32c52905994a1f65e06392fc2c45e4ae0f9b91bc.png
c2 = gf.Component("component_clean")
c2 = fix_underplot(
    component=c1,
    layers_extended=("SLAB150", "SLAB90"),
    layer_reference="WG",
    distance=0.1,
)
c2.plot()
../_images/dbbb4f3d0f6cf115d101625c37e9fabfcf30a466652b4975f45e1e7cdd0becb8.png

Outline#

The outline() function takes the polygons of the input geometry then performs an offset and “not” boolean operation to create an outline. The function returns polygons on a single layer – it does not respect layers.

import gdsfactory as gf

# Create a blank device and add two shapes
X = gf.Component("outline_demo")
X.add_ref(gf.components.cross(length=25, width=1, layer=(1, 0)))
X.add_ref(gf.components.ellipse(radii=[10, 5], layer=(2, 0)))

O = gf.geometry.outline(X, distance=1.5, precision=1e-6, layer=(3, 0))

# Plot the original geometry and the result
c = gf.Component("outline_compare")
c.add_ref(X)
c.add_ref(O).movex(30)
c.plot()
../_images/b474a40c8b0276a2a6bcb69272a343687903a13906cd936bb342062b0580c1a2.png

The open_ports argument opens holes in the outlined geometry at each Port location.

  • If not False, holes will be cut in the outline such that the Ports are not covered.

  • If True, the holes will have the same width as the Ports.

  • If a float, the holes will be widened by that value.

  • If a float equal to the outline distance, the outline will be flush with the port (useful positive-tone processes).

c = gf.components.L(width=7, size=(10, 20), layer=(1, 0))
c.plot()
../_images/e09fcf3e1615987a842ddc98c2594c96af90e94b10b3b1315b046a93ca57563a.png
# Outline the geometry and open a hole at each port
c = gf.geometry.outline(offsets, distance=5, open_ports=False, layer=(2, 0))  # No holes
c.plot()
../_images/ec6e933f78f4da3dbeee6666442f745cbb6c8b121f6073798df716658c8c1ad5.png
c = gf.geometry.outline(
    offsets, distance=5, open_ports=True, layer=(2, 0)
)  # Hole is the same width as the port
c.plot()
../_images/ec6e933f78f4da3dbeee6666442f745cbb6c8b121f6073798df716658c8c1ad5.png
c = gf.geometry.outline(
    offsets, distance=5, open_ports=10, layer=(2, 0)
)  # Change the hole size by entering a float
c.plot()
../_images/ec6e933f78f4da3dbeee6666442f745cbb6c8b121f6073798df716658c8c1ad5.png
c = gf.geometry.outline(
    offsets, distance=5, open_ports=5, layer=(2, 0)
)  # Creates flush opening (open_ports > distance)
c.plot()
../_images/ec6e933f78f4da3dbeee6666442f745cbb6c8b121f6073798df716658c8c1ad5.png

Invert#

Sometimes you need to define not what you keep (positive resist) but what you etch (negative resist). We have some useful functions to invert the tone. The gf.boolean.invert() function creates an inverted version of the input geometry. The function creates a rectangle around the geometry (with extra padding of distance border), then subtract all polygons from all layers from that rectangle, resulting in an inverted version of the geometry.

import gdsfactory as gf

E = gf.components.ellipse(radii=(10, 5))
D = gf.geometry.invert(E, border=0.5, precision=1e-6, layer=(2, 0))
D.plot()
../_images/e580ec12ee4d89cb771ae929cf22483d8741c2e1f513b548e6084e2dc314257f.png
c = gf.components.add_trenches(component=gf.components.coupler)
c.plot()
../_images/287ac83190a7e8606c63b545b373e332f839f727ef7ccbc30498ac25e7647e4c.png
c = gf.components.add_trenches(component=gf.components.ring_single)
c.plot()
../_images/38295344667714263de895ce3cd2bddf6dd16a0650bbe54b85ab195a3d3b446d.png

Union#

The union() function is a “join” function, and is functionally identical to the “OR” operation of gf.boolean(). The one difference is it’s able to perform this function layer-wise, so each layer can be individually combined.

import gdsfactory as gf

D = gf.Component("union")
e0 = D << gf.components.ellipse(layer=(1, 0))
e1 = D << gf.components.ellipse(layer=(2, 0))
e2 = D << gf.components.ellipse(layer=(3, 0))
e3 = D << gf.components.ellipse(layer=(4, 0))
e4 = D << gf.components.ellipse(layer=(5, 0))
e5 = D << gf.components.ellipse(layer=(6, 0))

e1.rotate(15 * 1)
e2.rotate(15 * 2)
e3.rotate(15 * 3)
e4.rotate(15 * 4)
e5.rotate(15 * 5)

D.plot()
2024-04-27 01:04:37.328 | WARNING  | gdsfactory.component:_write_library:1933 - UserWarning: Component union has invalid transformations. Try component.flatten_offgrid_references() first.
../_images/91ff96a9939feb4e014b37e631ccf3cb3e9370a819b2aad3d48f4377a2a6e5c0.png
# We have two options to unioning - take all polygons, regardless of
# layer, and join them together (in this case on layer (2,0) like so:
D_joined = gf.geometry.union(D, by_layer=False, layer=(2, 0))
D_joined
2024-04-27 01:04:37.518 | WARNING  | gdsfactory.klive:show:49 - UserWarning: Could not connect to klive server. Is klayout open and klive plugin installed?
union_componentunion_layer2__0: uid 5d871feb, ports [], references [], 1 polygons
../_images/bc103aba1010419255dc5a2804dce56ebce2c79581960e88724ae80ee69965c8.png
# Or we can perform the union operate by-layer
D_joined_by_layer = gf.geometry.union(D, by_layer=True)
D_joined_by_layer
union_0b116f46: uid 4e10181f, ports [], references [], 6 polygons
../_images/bfea6145ddc45439cef50308e05d9d457562c24dc7091c677b5f0ad2916c1869.png

XOR / diff#

The xor_diff() function can be used to compare two geometries and identify where they are different. Specifically, it performs a layer-wise XOR operation. If two geometries are identical, the result will be an empty Component. If they are not identical, any areas not shared by the two geometries will remain.

import gdsfactory as gf

A = gf.Component("A")
A.add_ref(gf.components.ellipse(radii=[10, 5], layer=(1, 0)))
A.add_ref(gf.components.text("A")).move([3, 0])

B = gf.Component("B")
B.add_ref(gf.components.ellipse(radii=[11, 4], layer=(1, 0))).movex(4)
B.add_ref(gf.components.text("B")).move([3.2, 0])
X = gf.geometry.xor_diff(A=A, B=B, precision=1e-6)

# Plot the original geometry and the result
# Upper left: A / Upper right: B
# Lower left: A and B / Lower right: A xor B "diff" comparison
D = gf.Component("xor_diff")
D.add_ref(A).move([-15, 25])
D.add_ref(B).move([15, 25])
D.add_ref(A).movex(-15)
D.add_ref(B).movex(-15)
D.add_ref(X).movex(15)
D.plot()
../_images/f51e12f7d870f1a42a3d5b17ca933e90db298628886eeae8bb38732d1a6aa277.png

Trim#

trim returns the portion of that component within that domain preserving all layers and (possibly) ports.

It’s like the opposite of “add_padding”, and also allows non-rectangular shapes for the padding removal.

Useful when resizing an existing component for simulations

c = gf.components.straight_pin(length=10, taper=None)
c.plot()
../_images/ebda2ab31e40ae9400c2016e822d4baf884639c2a5ecfba581829151d102963b.png
trimmed_c = gf.geometry.trim(component=c, domain=[[0, -5], [0, 5], [5, 5], [5, -5]])
trimmed_c.plot()
../_images/689cc5f14718bf182f383e309358276ed21f33893f16b047145c9e9b1aa7ef26.png

Importing GDS files#

gf.import_gds() allows you to easily import external GDSII files. It imports a single cell from the external GDS file and converts it into a gdsfactory component.

D = gf.components.ellipse()
D.write_gds("myoutput.gds")
D2 = gf.import_gds(gdspath="myoutput.gds", cellname=None, flatten=False)
D2.plot()
2024-04-27 01:04:38.486 | INFO     | gdsfactory.component:_write_library:2003 - Wrote to 'myoutput.gds'
../_images/2609997ae333c59f28bf61449041a4eddc9e80fc43132eeee0a4464aa3369d0a.png

Copying and extracting geometry#

E = gf.Component()
E.add_ref(gf.components.ellipse(layer=(1, 0)))
D = E.extract(layers=[(1, 0)])
D.plot()
2024-04-27 01:04:38.739 | WARNING  | gdsfactory.component:plot_klayout:1645 - UserWarning: Unnamed cells, 1 in 'Unnamed_d17d5533'
../_images/2609997ae333c59f28bf61449041a4eddc9e80fc43132eeee0a4464aa3369d0a.png
import gdsfactory as gf

X = gf.components.ellipse(layer=(2, 0))
c = X.copy()
c.plot()
2024-04-27 01:04:38.925 | WARNING  | gdsfactory.component:plot_klayout:1645 - UserWarning: Unnamed cells, 1 in 'Unnamed_67983bc2'
../_images/5e7105a3c6d26b55d6982ce47d20fb4634347d78e8ff1441f544d08a8f85fb71.png
c_copied_layers = gf.components.copy_layers(
    gf.components.straight, layers=((1, 0), (2, 0))
)
c_copied_layers.plot()
../_images/a4bf64b4f72200ccd632d2d2cf69a150246f0d2d21114b256b9b9d0796323d07.png

Import Images into GDS#

You can import your logo into GDS using the conversion from numpy arrays.

from gdsfactory.config import PATH
from gdsfactory.read.from_np import from_image
import gdsfactory as gf

c = from_image(
    PATH.module / "samples" / "images" / "logo.png", nm_per_pixel=500, invert=False
)
c.plot()
../_images/d8cdf8a5519809751da5c35b3762e0c0e7b37b8453342b44fe5a77117c7e8cbf.png
c = from_image(
    PATH.module / "samples" / "images" / "logo.png", nm_per_pixel=500, invert=True
)
c.plot()
../_images/325561b51649b4aae2601ed9bd0b2b5b11f325a8c03bec181e2fabe4079edbee.png

Dummy Fill / Tiling#

To keep constant density in some layers you can add dummy fill rectangles.

Custom fill cell#

You can use a custom cell as a fill.

import gdsfactory as gf
from gdsfactory.geometry.fill_klayout import fill


@gf.cell
def cell_with_pad():
    c = gf.Component()
    mzi = gf.components.mzi()
    mzi_with_padding = gf.add_padding_container(mzi)
    _ = c << mzi_with_padding
    pad = c << gf.components.pad(size=(2, 2))
    pad.movey(10)
    return c


c = cell_with_pad()
gf.remove_from_cache(c)
gdspath = c.write_gds("mzi_fill.gds")
c.plot()
2024-04-27 01:04:39.925 | INFO     | gdsfactory.component:_write_library:2003 - Wrote to 'mzi_fill.gds'
../_images/4473f77e671e8cf22376549f347d4cb562377eaa4799905723095a77ad5d93a5.png
spacing = 20
fill_name = fill(
    gdspath,
    fill_layers=("WG",),
    layer_to_fill=(67, 0),
    layers_to_avoid=(((1, 0), 0), ((49, 0), 0)),
    fill_cell_name="pad_size2__2",
    create_new_fill_cell=True,
    fill_spacing=(spacing, spacing),
    fill_size=(1, 1),
    include_original=True,
    layer_to_fill_margin=25,
)
c_fill = gf.import_gds(gdspath, cellname=fill_name)
c_fill.plot()
../_images/17c066c5294eb28087865600a49cc98cf35450a17e5a79aa391f2f469a527db5.png

Tiling processor#

For big layouts you can should use klayout tiling processor.

import kfactory as kf

import gdsfactory as gf
from kfactory.utils.fill import fill_tiled

c = kf.KCell("ToFill")
c.shapes(kf.kcl.layer(1, 0)).insert(
    kf.kdb.DPolygon.ellipse(kf.kdb.DBox(5000, 3000), 512)
)
c.shapes(kf.kcl.layer(10, 0)).insert(
    kf.kdb.DPolygon(
        [kf.kdb.DPoint(0, 0), kf.kdb.DPoint(5000, 0), kf.kdb.DPoint(5000, 3000)]
    )
)

fc = kf.KCell("fill")
fc.shapes(fc.kcl.layer(2, 0)).insert(kf.kdb.DBox(20, 40))
fc.shapes(fc.kcl.layer(3, 0)).insert(kf.kdb.DBox(30, 15))

# fill.fill_tiled(c, fc, [(kf.kcl.layer(1,0), 0)], exclude_layers = [(kf.kcl.layer(10,0), 100), (kf.kcl.layer(2,0), 0), (kf.kcl.layer(3,0),0)], x_space=5, y_space=5)
fill_tiled(
    c,
    fc,
    [(kf.kcl.layer(1, 0), 0)],
    exclude_layers=[
        (kf.kcl.layer(10, 0), 100),
        (kf.kcl.layer(2, 0), 0),
        (kf.kcl.layer(3, 0), 0),
    ],
    x_space=5,
    y_space=5,
)

gdspath = "mzi_fill.gds"
c.write(gdspath)
c = gf.import_gds(gdspath, cellname="ToFill")
c.plot()
2024-04-27 01:04:40.483 | INFO     | kfactory.utils.fill:fill_tiled:204 - filling ToFill with fill
2024-04-27 01:04:40.567 | INFO     | kfactory.utils.fill:fill_tiled:207 - done with filling ToFill
../_images/2ee02db724741dc21e9b5554efbfb5bfb33b6118a869cb8338f2902bf953728b.png