Photonic Design Automation¶

Photonic Design Automation (PDA) in PhotonForge is the project and team-collaboration layer that sits on top of the regular photonforge API. The plain library gives you components, technologies, and simulations; PDA adds a sync-server-backed workspace where teams building complex photonic chips can organize their work into named projects, import PDKs and libraries, create and version their own components, and share projects with other
team members.
Every design decision – from the PDK version used to each parameter change – can be tracked and retrieved. The motivation is traceability: months after tapeout, you can still recover exactly which component, at which parameters, against which PDK version, was fabricated – because the server stored it, not your local filesystem.

A project is the central object in PDA. It owns the components you create (each independently tagged and versioned), the libraries and PDKs it imports, and it can be shared with your team. The rest of this guide walks through that lifecycle end to end.
The PDA interface lives under photonforge.pda.
This guide assumes PhotonForge is installed and your Flexcompute API key is configured – see Getting Started.
Note: The PDA interface is experimental and may suffer breaking changes in the 1.4.* version family for improved usability. Please check the changelog after any updates.
Setup¶
We start by importing PhotonForge and the PDA submodule. The LiveViewer is optional but useful for inspecting components as we add them to the project.
[1]:
import numpy as np
import matplotlib.pyplot as plt
import photonforge as pf
import photonforge.pda as pda
from photonforge.live_viewer import LiveViewer
# The MZI factory and the PDK technology are defined outside the project's stored
# module, so PhotonForge notes they cannot be regenerated from source later; we treat
# each saved revision as a fixed design here and silence that notice.
import warnings
warnings.filterwarnings("ignore", message="The parametric function for")
pf.config.default_mesh_refinement = 10 # coarse mesh for a fast guide; the default (20) is needed for accurate S-parameters
pf.config.use_local_mode_solver = True
wavelengths = np.linspace(1.5, 1.6, 51)
freqs = pf.C_0 / wavelengths
viewer = LiveViewer()
LiveViewer started at http://localhost:65308
Browsing available libraries and projects¶
The first PDA call connects to the sync server automatically. pda.list_libraries returns every importable library – foundry PDKs and any project that has been published as a version. pda.list_projects returns every project visible on the server, along with the documentId you need to load it later.
[2]:
print("Available libraries:")
for lib in pda.list_libraries():
print(f" {lib['name']:30s} v{lib.get('version', '?'):8s} (id={lib['documentId']})")
Available libraries:
SiEPIC EBeam v1.2.0 (id=3jBW9X8Ki8t3PvQMXgK3ZfA5CkHs)
SiEPIC EBeam SiN v1.2.0 (id=3a7uKcq1ZxCjEm15gj1BQuzQn61c)
Luxtelligence LNOI400 v2.1.0 (id=45krJGyQdGcgiBxQwLWemwx2pqWa)
Luxtelligence LTOI300 v2.1.0 (id=4MpixynBEvyHNh2bhgrkvZvWue2V)
Abstract Components v1.4.2 (id=3vjWBDhVd8CcS64NJdJYw7SNS9SS)
[3]:
print("Available projects:")
for p in pda.list_projects():
print(f" {p['name']:30s} (id={p['documentId']})")
Available projects:
SiEPIC EBeam (id=3jBW9X8Ki8t3PvQMXgK3ZfA5CkHs)
SiEPIC EBeam SiN (id=3a7uKcq1ZxCjEm15gj1BQuzQn61c)
Luxtelligence LNOI400 (id=45krJGyQdGcgiBxQwLWemwx2pqWa)
Luxtelligence LTOI300 (id=4MpixynBEvyHNh2bhgrkvZvWue2V)
Abstract Components (id=3vjWBDhVd8CcS64NJdJYw7SNS9SS)
Creating a project¶
pda.create_project registers a new, empty project on the server and returns a Project handle.
[4]:
project = pda.create_project(
"pda-guide-mzi",
description="PDA guide demo project",
)
print(f"Created project '{project.name}' (id={project.id})")
print(f" components: {dict(project.components(origin='self'))}")
print(f" libraries: {project.get_library_info()}")
Created project 'pda-guide-mzi' (id=4Tu9bmTFuhxFaEzJfNvULY8wpZD4)
components: {}
libraries: []
Importing a PDK library¶
A fresh project has no PDK attached. project.add_library pulls a versioned PDK into the project so its technologies and components become available.
Note: This guide loads the SiEPIC EBeam PDK from the sync server, so the siepic_forge pip package is not needed. The PDK ships both as that pip package and as this PDA library – use one or the other, since installing both can cause version conflicts.
[5]:
project.add_library(name="SiEPIC EBeam", version="1.2.0")
for lib in project.get_library_info():
print(f" {lib['name']} v{lib.get('version', '?')}")
SiEPIC EBeam v1.2.0
Browsing imported content¶
project.components and project.technologies inspect what is reachable from the project.
The origin argument filters by source: pass a library name ("SiEPIC EBeam") to browse that PDK, or "self" to see only what this project owns.
[6]:
print("Technologies from SiEPIC EBeam:")
for name in project.technologies(origin="SiEPIC EBeam"):
print(f" {name}")
print("\nFirst few components from SiEPIC EBeam:")
for name in list(project.components(origin="SiEPIC EBeam"))[:10]:
print(f" {name}")
Technologies from SiEPIC EBeam:
SiEPIC EBeam Si
First few components from SiEPIC EBeam:
taper_si_simm_1310
ebeam_gc_te1550
ebeam_y_1310
ebeam_terminator_te1310
ebeam_routing_taper_te1550_w=500nm_to_w=3000nm_L=20um
ebeam_routing_taper_te1550_w=500nm_to_w=3000nm_L=40um
ebeam_adiabatic_tm1550
ebeam_terminator_tm1550
ebeam_y_adiabatic
GC_TM_1310_8degOxide_BB
Pull the technology object out of the project and assign it as the default for any parametric components we build.
[7]:
tech = project.technologies(name="SiEPIC EBeam Si", origin="SiEPIC EBeam")
pf.config.default_technology = tech
print(f"Using technology: {tech.name} v{tech.version}")
Using technology: SiEPIC EBeam Si v1.2.0
Defining a component¶
We will build an MZI factory and use it to demonstrate the PDA workflow. The factory pulls a directional coupler from the imported PDK and stitches it together with parametric bends and straights via pf.component_from_netlist.
Note: Pass a fixed name into the factory (here "MZI") when you want successive revisions of a component to share the same project document. Without a stable name, the parametric_component decorator mints a fresh name for every call and project.update will not be able to match the new version to the old one.
[8]:
@pf.parametric_component(name_prefix="MZI")
def mzi(
*,
upper_arm_length: float = 10.0,
lower_arm_length: float = 20.0,
name: str | None = None,
):
port_spec = "TE_1550_500"
# NOTE: We pull in the broadband directional coupler
# from the imported SiEPIC EBeam library
bdc = project.components(name="ebeam_bdc_te1550", origin="SiEPIC EBeam")
bdc.add_model(pf.DirectionalCouplerModel(), "DC")
bdc.activate_model("DC", classification="optical")
bend = pf.parametric.bend(port_spec=port_spec, radius=5.0)
ubend = pf.parametric.bend(port_spec=port_spec, radius=5.0, angle=180.0)
upper_half = pf.parametric.straight(port_spec=port_spec, length=upper_arm_length / 2)
lower_half = pf.parametric.straight(port_spec=port_spec, length=lower_arm_length / 2)
mirrored = {"x_reflection": True}
return pf.component_from_netlist({
"name": "" if name is None else name,
"instances": {
"in": bdc,
"b1": bend, "s1": upper_half, "ub1": ubend,
"s2": upper_half, "b2": bend,
"b3": {"component": bend, **mirrored},
"s3": lower_half, "ub2": ubend,
"s4": lower_half,
"b4": {"component": bend, **mirrored},
"out": {"component": bdc, **mirrored},
},
"connections": [
(("b1", "P0"), ("in", "P3")),
(("s1", "P0"), ("b1", "P1")),
(("ub1", "P1"), ("s1", "P1")),
(("s2", "P0"), ("ub1", "P0")),
(("b2", "P0"), ("s2", "P1")),
(("b3", "P0"), ("in", "P2")),
(("s3", "P0"), ("b3", "P1")),
(("ub2", "P0"), ("s3", "P1")),
(("s4", "P0"), ("ub2", "P1")),
(("b4", "P0"), ("s4", "P1")),
(("out", "P2"), ("b4", "P1")),
],
"ports": [
("in", "P0", "INPUT"),
("in", "P1", "INPUT_CROSS"),
("out", "P0", "OUTPUT"),
("out", "P1", "OUTPUT_CROSS"),
],
"models": [pf.CircuitModel()],
})
MZI_NAME = "MZI"
Saving a component with a tag¶
Build a balanced MZI and store it in the project with project.add.
The optional tag argument is a human-readable label that lets you find this revision later by name rather than by version number. Adding a component with a tag also triggers the auto-generated project_tag template, which writes a timestamped tag to the project document for the audit trail.
Note: The mzi factory is defined inline in this notebook rather than stored in the project’s own module. Each saved revision keeps its exact geometry and stays fully usable, but a later session would not be able to re-run the factory at new parameters – this guide treats every saved revision as a fixed design. Storing parametric source in a project so its components can be regenerated later is a more advanced topic, beyond this guide’s scope.
[9]:
mzi_v1 = mzi(upper_arm_length=10.0, lower_arm_length=10.0, name=MZI_NAME)
project.add(mzi_v1, tag="balanced")
print(f"Saved {mzi_v1.name!r}")
print(f" component tags: {project.list_tags(mzi_v1)}")
print(f" project auto-tags: {project.list_tags()}")
viewer(mzi_v1)
No schema for 'symmetry: typing.Annotated[collections.abc.Sequence[int], _]' in 'Tidy3DModel'.
No schema for 'bounds: typing.Annotated[collections.abc.Sequence[collections.abc.Sequence[float | None]], _]' in 'Tidy3DModel'.
Saved 'MZI'
component tags: ['balanced']
project auto-tags: ['20260624-094128-MZI-balanced']
[9]:
Versioning a component¶
Tags are mutable – you can move or remove them. Versions are not: project.add_version pins the current state under an immutable label.
Pass target=component.name to version a single component. Omit target to version the project itself.
[10]:
project.add_version("1.0.0", target=mzi_v1.name)
print(f"Component versions: {project.list_versions(mzi_v1)}")
Component versions: ['1.0.0']
Loading a component back¶
project.load retrieves a stored (tagged and/or versioned) component. The call always returns exactly one component and raises RuntimeError on 0 or more than one match.
We simulate the reloaded component to confirm that a round-trip through the server preserves the design exactly.
[11]:
loaded_v1 = project.load(MZI_NAME, version="1.0.0")
print(f"Reloaded {loaded_v1.name} kwargs={loaded_v1.parametric_kwargs}")
s_v1 = loaded_v1.s_matrix(frequencies=freqs)
fig, _ = pf.plot_s_matrix(s_v1, input_ports=["INPUT"], y="dB")
fig.suptitle("MZI v1.0.0 (balanced)")
plt.show()
Reloaded MZI kwargs={'upper_arm_length': 10.0, 'lower_arm_length': 10.0, 'name': 'MZI'}
Progress: 100%
Updating a component¶
To save a new revision against the same component document, build the new component with the same name and call project.update. PhotonForge matches the existing document by name; the previous version stays reachable through project.load.
[12]:
mzi_v2 = mzi(upper_arm_length=10.0, lower_arm_length=50.0, name=MZI_NAME)
project.update(mzi_v2, tag="unbalanced")
project.add_version("2.0.0", target=mzi_v2.name)
print(f"Component tags: {project.list_tags(mzi_v2)}")
print(f"Component versions: {project.list_versions(mzi_v2)}")
viewer(mzi_v2)
Component tags: ['unbalanced', 'balanced']
Component versions: ['2.0.0', '1.0.0']
[12]:
Both versions remain independently loadable.
[13]:
v1 = project.load(MZI_NAME, version="1.0.0")
v2 = project.load(MZI_NAME, version="2.0.0")
print(f"v1.0.0 kwargs={v1.parametric_kwargs}")
print(f"v2.0.0 kwargs={v2.parametric_kwargs}")
s_v2 = v2.s_matrix(frequencies=freqs)
fig, _ = pf.plot_s_matrix(s_v2, input_ports=["INPUT"], y="dB")
fig.suptitle("MZI v2.0.0 (unbalanced)")
plt.show()
v1.0.0 kwargs={'upper_arm_length': 10.0, 'lower_arm_length': 10.0, 'name': 'MZI'}
v2.0.0 kwargs={'upper_arm_length': 10.0, 'lower_arm_length': 50.0, 'name': 'MZI'}
Progress: 100%
Publishing the project as a library¶
Calling project.add_version without a target pins the whole project under that version. Versioned projects appear in pda.list_libraries and can be imported by other projects via project.add_library.
[14]:
project.add_version("1.0.0")
print(f"Published '{project.name}' v1.0.0")
print(f"Project versions: {project.list_versions()}")
# Now the project is importable from any other project on this server.
print("\nIs the project visible as a library?")
for lib in pda.list_libraries():
if lib['name'] == project.name:
print(f" yes: {lib['name']} v{lib.get('version', '?')}")
Published 'pda-guide-mzi' v1.0.0
Project versions: ['1.0.0']
Is the project visible as a library?
yes: pda-guide-mzi v1.0.0
Reopening a project in a fresh session¶
In a later session, pda.load_project reattaches to the project. Use the documentId rather than the name – names are not unique on the server, but ids are.
Loading without version= returns the latest, editable state of the project:
[15]:
head = pda.load_project(project_id=project.id)
print(f"Loaded '{head.name}' is_read_only={head.is_read_only}")
print(f" versions: {head.list_versions()}")
print(f" tags: {head.list_tags()}")
Loaded 'pda-guide-mzi' is_read_only=False
versions: ['1.0.0']
tags: ['20260624-094139-MZI-unbalanced', '20260624-094128-MZI-balanced']
Read-only snapshots¶
Loading with version= returns an immutable snapshot of the project at that release. Any mutating call – set, add, update, add_version – raises RuntimeError. This is the guarantee that a downstream user cannot accidentally edit the design that taped out.
To go back to the latest, editable state of the same project, call load_latest on the handle.
[16]:
ro = pda.load_project(project_id=project.id, version="1.0.0")
print(f"is_read_only: {ro.is_read_only}")
try:
ro.set(description="this will fail")
except RuntimeError as e:
print(f"Mutation blocked (as expected): {e}")
ro.load_latest()
print(f"After load_latest(): is_read_only={ro.is_read_only}")
is_read_only: True
Mutation blocked (as expected): Read only project/library 'pda-guide-mzi' cannot be modified.
After load_latest(): is_read_only=False
Sharing a project or library with your team¶
PDA shares at the level of an organization (tenant): granting access makes a project — or, once it is versioned, a library — available to everyone in your organization. Use project.grant_permission with visibility="organization"; the grantee_id defaults to your current tenant, so you usually do not pass it.
Roles:
viewer— read-only access (load the project, import it as a library).editor— read access plus the ability to modify the project.owner— held by the creator; reassign it with transfer_ownership.
pda.user_info() returns (user_id, tenant_id); a tenant_id of None means your account is not part of an organization, in which case organization sharing is unavailable.
Notes:
Sharing a library is the same call: publish the project with a version (the Publishing step above), then grant organization access so teammates can
add_libraryit.Per-user sharing (one specific colleague) is not currently supported by the server — the organization is the unit of sharing. Manage grants with list_permissions, update_permission, and revoke_permission.
Public (whole-platform) sharing is intentionally disabled in the SDK; contact Flexcompute support if you need a public library.
[17]:
# Who am I, and which organization (tenant) am I in?
user_id, tenant_id = pda.user_info()
print(f"user_id={user_id} tenant_id={tenant_id}")
# Share this project with everyone in your organization, read-only.
# grantee_id defaults to your current tenant, so it can be omitted.
project.grant_permission(visibility="organization", role="viewer")
# Inspect the current sharing permissions.
print("Permissions:")
for perm in project.list_permissions():
print(f" {perm}")
# To change or remove a grant later, use the permission's id:
# project.update_permission(permission_id, role="editor")
# project.revoke_permission(permission_id)
user_id=user-11111111-1111-1111-1111-111111111111 tenant_id=22222222-2222-2222-2222-222222222222
Permissions:
{'id': 'aaaaaaaa-0000-0000-0000-000000000001', 'documentId': '4Tu9bmTFuhxFaEzJfNvULY8wpZD4', 'granteeId': 'user-11111111-1111-1111-1111-111111111111', 'role': 'owner', 'createdBy': 'user-11111111-1111-1111-1111-111111111111', 'createdAt': '2026-06-24T13:41:17.204Z', 'visibility': 'private'}
{'id': 'aaaaaaaa-0000-0000-0000-000000000002', 'documentId': '4Tu9bmTFuhxFaEzJfNvULY8wpZD4', 'granteeId': '22222222-2222-2222-2222-222222222222', 'role': 'viewer', 'createdBy': 'user-11111111-1111-1111-1111-111111111111', 'createdAt': '2026-06-24T13:41:47.446Z', 'visibility': 'organization'}
Disconnecting¶
The project state lives on the server; closing the connection does not affect it. pda.stop releases the session.
[18]:
pda.stop()
viewer.stop()
LiveViewer stopped.
Recap¶
PDA layers five capabilities on top of the regular PhotonForge workflow:
A server-backed workspace. Your projects – and the components, technologies, libraries, and version history they hold – live on the sync server rather than on your local filesystem. Reattach to any project from any machine or session with load_project and its
documentId.Versioning and an audit trail, on your terms. You decide when to record state: add and update save component revisions, add_tag and add_version label them, and each of those calls also stamps a timestamped tag onto the project through the
project_tagtemplate. Anything you have recorded can be retrieved later with list_tags, list_versions, and load. Tags are movable labels; versions are immutable.Immutability where it counts. A project or component loaded by
version=comes back read-only – mutating calls raiseRuntimeError, so a taped-out design cannot be edited by accident. load_latest returns you to the latest, editable state.Projects become libraries. Versioning a whole project (add_version with no
target) publishes it; other projects then add_library it and build on its components and technologies – exactly the way this guide consumed the SiEPIC EBeam PDK.Team collaboration. Share a project or library across your organization with grant_permission and the
viewer/editor/ownerroles.
Reserve clear, descriptive names for projects you intend to publish as libraries, since other projects import them by name.
API reference¶
Every function and method used in this guide:
Connecting and browsing
pda.user_info – your user id and organization (tenant) id
pda.list_libraries – libraries and PDKs available to import
pda.list_projects – projects visible to you
pda.stop – close the connection
Creating and inspecting a project
pda.create_project – register a new project
pda.load_project – reattach to an existing project (optionally at a
version/tag)Project.add_library – import a PDK or a published project
Project.get_library_info – list the project’s imported libraries
Project.components / Project.technologies – browse reachable content
Project.set – edit project metadata (name, description, labels)
Saving, tagging, and versioning
Project.add – store a new component or technology
Project.update – save a new revision of an existing one
Project.add_version – pin an immutable version of a component, or of the whole project
Project.list_tags / Project.list_versions – see what has been recorded
Project.load – retrieve a stored revision by
tagorversionProject.load_latest – return to the latest, editable state of a project
Sharing
Project.grant_permission – share a project or library with your organization
Project.list_permissions – inspect the current grants
Project.update_permission / Project.revoke_permission – change or remove a grant
See also:
Project – the full Project API
Technology – how PDKs describe their fabrication process
PDK Components – how to use components imported from a PDK
Parametric Component Library – how to build parametric components like the MZI factory above