Grid and Pack

This notebook demonstrates how to arrange layout primitives and components using photonforge.grid_layout() and photonforge.pack_layout(). The grid_layout function is ideal for creating structured arrays, such as parameter sweeps, with precise control over spacing and alignment. In contrast, pack_layout provides a flexible way to efficiently organize a large number of arbitrary shapes into compact bins, optimizing space usage based on customizable heuristics. Together, these tools enable powerful layout automation workflows.
import numpy as npimport photonforge as pf
tech = pf.basic_technology()pf.config.default_technology = techThe grid_layout() function arranges a list (or 2D array) of objects—such as components, shapes, or references—into a structured grid. This is particularly useful for visualizing parameter sweeps or organizing elements for inspection and comparison.
In the following example, we create a diverse set of layout elements by combining linear tapers (with varying input/output widths) and circular rings (with varying radius). These are then arranged in a grid to demonstrate the flexibility and expressiveness of grid_layout.
from photonforge import Circle, grid_layoutfrom photonforge.stencil import linear_taper
# Create a list to hold taper components with different width combinationsshape_list = []for width1 in [1, 3, 6, 9]: for width2 in [1, 2]: # Create a taper of length 10 with varying input/output widths D = linear_taper(length=10, widths=(width1, width2)) shape_list.append(D)
# Add several circular rings to the shape listfor radius in [2, 3, 4, 5]: # Create a ring with given outer and inner radius ring = Circle(radius=radius, inner_radius=radius - 1) shape_list.append(ring)
# Arrange all shapes in a gridc = grid_layout(shape_list)
# Visualize the arranged grid layoutcYou can control the grid layout using several arguments:
gapsets horizontal and vertical spacing between items.shapedefines the number of columns and rows in the grid.align_xandalign_ycontrol the alignment of each object within its grid cell.directionspecifies the order in which items fill the grid (e.g., left-to-right then top-to-bottom).layerassigns a common layer to all included geometry if applicable.
# Set 1 unit gap, align left/top, direction left-to-right then top-to-bottom# Add resulting layout to layer (2, 0)c = grid_layout( shape_list, gap=(1, 1), shape=(4, 3), align_x="center", align_y="top", direction="lr-tb", layer=(2, 0),)
# Visualize the arranged grid layoutcThe pack_layout() function arranges a sequence of objects—such as components, shapes, or references—into one or more rectangular bins, optimizing for space utilization. This is especially useful when you need to fit a collection of primitives into a constrained area or when exploring different packing heuristics.
In the following example, we construct a list of cross-shaped primitives with increasing arm lengths, then pack them into a bin without explicit size limits. The result shows how we can efficiently place all shapes into a single packed component.
from photonforge import pack_layoutfrom photonforge.stencil import cross
# Set random seed for reproducibilitynp.random.seed(4)
# Create a list of crosses with varying arm lengths and widthsD_list = [cross(arm_length=i, arm_width=i) for i in range(1, 10)]
# Pack the crosses into binsD_packed_list = pack_layout(D_list)
# Extract the first packed component (only one bin created)D = D_packed_list[0]
# Visualize the packed layoutDYou can control the packing behavior with several arguments:
gapsets the minimum distance between adjacent items.aspect_ratiodefines the desired width:height ratio of each bin.grow_factordetermines how aggressively the bin size increases during packing; values closer to 1 yield tighter fits at the cost of extra computation.sortingspecifies how to order objects before packing (e.g.,"area"to place larger items first).allow_rotationenables or disables 90° rotations of shapes to improve fit.methodselects a heuristic for choosing free slots (e.g.,"blsf"for Best Long Side Fit).layerassigns a common layer to all packed geometry if desired.nameprovides a template for naming bins when multiple are generated.
# Pack the crosses into bins with specified parametersD_packed_list = pack_layout( D_list, # List of shapes to pack gap=1.25, # Minimum distance between adjacent shapes aspect_ratio=2, # Desired width:height ratio of each bin grow_factor=1.05, # Controls incremental bin size increase sorting="area", # Sort shapes by area before packing method="blsf", # Use best long side fit heuristic allow_rotation=False, # Do not rotate shapes during packing layer=(2, 0),)
# Extract the first packed component (only one bin created)D = D_packed_list[0]
# Visualize the packed layoutDAs another example, we define a parametric component that connects two grating couplers using an s_bend waveguide.
from photonforge.parametric import s_bendfrom photonforge.stencil import as_component
@pf.parametric_component()# Define a parametric component: 2 gratings connected by an s-benddef s_bend_grating(length=20.0, offset=5.0): c = pf.Component()
# Create the first grating component using the stencil grating = as_component( stencil="grating", layer=(2, 0), period=0.6, num_periods=10, width=5, taper_length=10, taper_width=0.5, )
# Add a slab layer around the grating using envelope slab = pf.envelope( grating, offset=1, trim_x_min=True, trim_x_max=True, ) grating.add((1, 0), slab)
# Automatically detect and assign ports to the grating grating.add_port(grating.detect_ports([tech.ports["Strip"]]))
# Add two grating references to the main component g_ref1 = c.add_reference(grating) g_ref2 = c.add_reference(grating)
# Create an s-bend with specified length and offset sb = s_bend(port_spec="Strip", length=length, offset=offset) ref_sb = c.add_reference(sb)
# Connect s-bend input to first grating and output to second grating ref_sb.connect("P0", g_ref1["P0"]) g_ref2.connect("P0", ref_sb["P1"])
return c
# Instantiate the component and visualize itc = s_bend_grating()cAs an example, assume that we want to evaluate different s-bend geometries on a single test chip, we generate 100 instances of the s_bend_grating component with randomized length and offset parameters. We then use pack_layout() to efficiently organize these instances within a confined chip area. The packing function ensures that the layout is space-efficient, adheres to spacing constraints, and splits the components into bins if necessary. This approach is ideal for layout prototyping and measurement-ready chip designs.
# Set a random seed for reproducibilitynp.random.seed(2)
# Create a list of s-bend grating components with random length and offsetD_list = []for n in range(100): (length, offset) = tuple(np.random.rand(2) * n + 2) sg = s_bend_grating(length=length, offset=offset) D_list.append(sg)
# Pack the components into bins using spatial heuristicsD_packed_list = pack_layout( D_list, # List of s-bend grating components to pack gap=5, # Minimum spacing between adjacent components aspect_ratio=1, # Use square bins max_size=(400, 400), # Limit bin size to 400×400 grow_factor=1.1, # Controls bin size growth during packing sorting="best", # Use smart heuristic for placement order method="bssf", # Best Short Side Fit for packing efficiency)
# Print how many bins were created during packingprint(len(D_packed_list))2c = pf.grid_layout(D_packed_list)c