Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ Each layer requires:
* `z`: Z-position in micrometers
* `height`: Layer thickness in micrometers

An optional field with metadata can be added to customize Blender's behavior when reading the file:

```yaml
pdk_config:
name: SkyWater SKY130 PDK
description: SkyWater SKY130 130nm process
order: 3
def_color: realistic
```

All metadata is optional:
* `name/description` - Extra details of PDK. Takes file name if unset
* `order` - Sets order of PDKs as they will appear in Blender's dropdown. Defaults to "0"
* `def_color` - Sets default color scheme to be used for the PDK. Defaults to alphabetic order

#### Parameters

- `pdk` - Process Design Kit specification (e.g., `ihp-sg13g2`)
Expand Down
137 changes: 79 additions & 58 deletions import_gdsii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,38 +30,46 @@
# ============================================================================

# PDK Configuration paths
PDK_CONFIGS = {
'IHP_SG13G2': {
'name': 'IHP Open PDK (SG13G2)',
'config_path': 'configs/ihp-sg13g2.yaml',
'color_path': 'configs/colors/ihp-sg13g2/'
},
'IHP_SG13CMOS5L': {
'name': 'IHP Open PDK (SG13CMOS5L)',
'config_path': 'configs/ihp-sg13cmos5l.yaml',
'color_path': 'configs/colors/ihp-sg13cmos5l/'
},
'SKY130': {
'name': 'SkyWater SKY130 PDK',
'config_path': 'configs/sky130.yaml',
'color_path': 'configs/colors/sky130/'
},
'GF180MCU': {
'name': 'GlobalFoundries GF180MCU PDK',
'config_path': 'configs/gf180mcu.yaml',
'color_path': 'configs/colors/gf180mcu/'
},
'SIEPIC_EBEAM': {
'name': 'SiEPIC EBeam PDK',
'config_path': 'configs/siepic.yaml',
'color_path': 'configs/colors/siepic/'
},
'LNOI400': {
'name': 'Luxtelligence LNOI400 PDK',
'config_path': 'configs/lnoi400.yaml',
'color_path': 'configs/colors/lnoi400/'
},
}
def load_pdk_configs(configs_dir=Path(__file__).parent / "configs"):
pdks = {}
yaml_files = [
f for f in os.listdir(configs_dir)
if f.endswith('.yaml') and os.path.isfile(configs_dir / f)
]

for yaml_file in yaml_files:
file_name = yaml_file[:-5]
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
key = file_name.upper().replace('-', '_')

config_path = configs_dir / yaml_file
with open(config_path) as f:
config = yaml.safe_load(f)

pdks[key] = {
'config_path': config_path, # Don't acces per-layer info yet
'color_path': configs_dir / 'colors' / file_name,
'file_name': file_name,
}
if "pdk_config" in config:
# name, description, order, def_color
pdks[key].update(**config["pdk_config"])
# Fill in defaults if not set
if "name" not in pdks[key]:
pdks[key]["name"] = key
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
if "description" not in pdks[key]:
pdks[key]["description"] = key
if "order" not in pdks[key]:
pdks[key]["order"] = 0
if "def_color" not in pdks[key]:
pdks[key]["def_color"] = ""

# Return sorted "pdks" dictionary by "order" and then alphabetically
return dict(sorted(
pdks.items(),
key=lambda kv: (kv[1]["order"], kv[0])
))

PDK_CONFIGS = load_pdk_configs()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use a constant variable for those information.



# ============================================================================
Expand Down Expand Up @@ -232,7 +240,10 @@ def _create_merged_gds(gds_path, layerstack, tmp_dir):
merged_layout.dbu = layout.dbu
merged_top_cell = merged_layout.create_cell(top_cell.name)

for data in layerstack.values():
print("layerstack",layerstack)
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
for layer_name, data in layerstack.items():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why skipping merge GDS if pdk_config is inside this variable?

@Gerard-Taulats Gerard-Taulats Jul 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"layerstack.items()" is looping through the contents inside each .YAML file. Before this PR, only layer names were expected, but now that we have a non-layer name with non-layer contents, this should be explicitly skipped

if layer_name=="pdk_config":
continue
layer = (data['index'], data['type'])
layer_index = layout.layer(*layer)
region = db.Region(top_cell.begin_shapes_rec(layer_index))
Expand Down Expand Up @@ -335,21 +346,34 @@ def create_extruded_layer(report, gds_path, z, height, layer, name, color, unit=
# PRE-IMPORT PDK SELECTION DIALOG
# ============================================================================

def get_pdk_list(self, context):
"""Dynamically generate pdk list"""

pdk_list = []
for k,v in PDK_CONFIGS.items():
pdk_list.append(
(k, v["name"], v["description"])
)
return pdk_list

def get_color_schemes(self, context):
"""Dynamically generate color scheme list based on selected PDK"""
items = []
# TODO: Load only when accessing list or when opening file explorer, not every refresh. Or add a refresh button

pdk = getattr(context.scene, 'gdsii_pdk_selection', 'IHP_SG13G2')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: nit: this fallback is still the old hardcoded 'IHP_SG13G2' — the rest of this PR switched to next(iter(PDK_CONFIGS)) for the same purpose (lines 625, 638, 777).

Suggested change
pdk = getattr(context.scene, 'gdsii_pdk_selection', 'IHP_SG13G2')
pdk = getattr(context.scene, 'gdsii_pdk_selection', next(iter(PDK_CONFIGS)))

addon_dir = Path(__file__).parent
color_path = addon_dir / PDK_CONFIGS.get(pdk, {}).get('color_path', pdk)
schemes = [('realistic', 'Realistic', 'Realistic color scheme')]
pdf_dict = PDK_CONFIGS.get(pdk, {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call load_pdk_configs instead of the constant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having mixed feelings about this. This constant is being used across 7 functions. While that'd help to maintain latest folder status, I think it's a bit too much to call this every time. What's the usual way on handling these cases? Is it the way you suggest?🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function should be run inside "register_properties", GDSIIPreImportDialog class and Import GDSII class once, and pass around data for functions that need it. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, loading it once and storing it internally is the right way. I think I already did this with other data.

color_path = addon_dir / pdf_dict.get('color_path', pdk)
schemes = []
for file in color_path.glob('*yaml'):
# Make sure realistic is the default choice
if file.stem == 'realistic':
continue
color_file = yaml.safe_load(file.read_text(encoding='utf-8'))
schemes.append((file.stem, color_file.get('name', file.stem),
color_file.get('description', file.stem)))
if file.stem==pdf_dict["def_color"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: bug: if pdk isn't a valid key (e.g. the stale fallback below), pdf_dict is {} and this raises KeyError.

Suggested change
if file.stem==pdf_dict["def_color"]:
if file.stem==pdf_dict.get("def_color"):


schemes.insert(0,(file.stem, color_file.get('name', file.stem),
Comment thread
dnltz marked this conversation as resolved.
Resolved
color_file.get('description', file.stem)))
else:
schemes.append((file.stem, color_file.get('name', file.stem),
color_file.get('description', file.stem)))
return schemes


Expand All @@ -362,15 +386,7 @@ class GDSIIPreImportDialog(bpy.types.Operator):
pdk_selection: EnumProperty(
name="PDK",
description="Process Design Kit to use for layer stack",
items=[
('IHP_SG13G2', "IHP Open PDK SG13G2", "IHP SG13G2 130nm BiCMOS process"),
('IHP_SG13CMOS5L', "IHP Open PDK SG13CMOS5L", "IHP SG13CMOS5L 130nm CMOS5L process"),
('SKY130', "SkyWater SKY130 PDK", "SkyWater SKY130 130nm process"),
('GF180MCU', "GlobalFoundries GF180MCU PDK", "GlobalFoundries GF180MCU 180nm process"),
('SIEPIC_EBEAM', "SiEPIC EBeam PDK", "SiEPIC EBeam silicon photonics PDK (220 nm SOI)"),
('LNOI400', "Luxtelligence LNOI400 PDK", "Luxtelligence LNOI400 thin-film lithium niobate (X-cut, 400 nm LN, 200 nm etch) photonics PDK"),
],
default='IHP_SG13G2',
items=get_pdk_list,
)

use_custom_config: BoolProperty(
Expand Down Expand Up @@ -407,10 +423,12 @@ def update_config_path(self):
config_file = addon_dir / pdk_info['config_path']
if config_file.exists():
self.custom_config_path = str(config_file)
default_color = addon_dir / pdk_info['color_path'] / 'realistic.yaml'
if pdk_info["def_color"] == "":
return
default_color = addon_dir / pdk_info['color_path'] / f"{pdk_info['def_color']}.yaml"
if default_color.exists():
self.custom_color_path = str(default_color)

Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
def draw(self, context):
layout = self.layout

Expand Down Expand Up @@ -499,7 +517,7 @@ class ImportGDSII(bpy.types.Operator, ImportHelper):
color_scheme: EnumProperty(
name="Color Scheme",
description="Select color scheme for the PDK",
items=get_color_schemes
items=get_color_schemes,
)

# Crop region options
Expand Down Expand Up @@ -593,7 +611,7 @@ def draw(self, context):
box = layout.box()
box.label(text="Selected PDK:", icon='PRESET')
if not use_custom:
pdk = getattr(context.scene, 'gdsii_pdk_selection', 'IHP_SG13G2')
pdk = getattr(context.scene, 'gdsii_pdk_selection', next(iter(PDK_CONFIGS)))
pdk_name = PDK_CONFIGS.get(pdk, {}).get('name', pdk)
else:
pdk_name = "Custom"
Expand All @@ -606,7 +624,7 @@ def import_gdsii(self, context, filepath):
"""Main import function"""
try:
# Get PDK settings from scene
pdk_selection = getattr(context.scene, 'gdsii_pdk_selection', 'IHP_SG13G2')
pdk_selection = getattr(context.scene, 'gdsii_pdk_selection', next(iter(PDK_CONFIGS)))
use_custom = getattr(context.scene, 'gdsii_use_custom_config', False)
custom_config_path = getattr(context.scene, 'gdsii_custom_config_path', '')
custom_color_path = getattr(context.scene, 'gdsii_custom_color_path', '')
Expand All @@ -618,7 +636,8 @@ def import_gdsii(self, context, filepath):
# Use built-in PDK config
pdk_info = PDK_CONFIGS.get(pdk_selection, {})
addon_dir = Path(__file__).parent
yamlfile = addon_dir / pdk_info.get('config_path', 'configs/ihp-sg13g2.yaml')
yamlfile = addon_dir / pdk_info.get('config_path', 'configs/'+
pdk_info["file_name"]+'.yaml')
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved

# Load layer stack configuration
if not yamlfile.is_file():
Expand Down Expand Up @@ -668,7 +687,7 @@ def import_gdsii(self, context, filepath):
if use_custom:
colorfile = Path(custom_color_path)
else:
color_dir = addon_dir / pdk_info.get('color_path', 'configs/colors/ihp-sg13g2')
color_dir = addon_dir / pdk_info.get('color_path', 'configs/colors/'+pdk_info["file_name"])
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
colorfile = color_dir / f"{self.color_scheme}.yaml"
if not colorfile.is_file():
self.report({'ERROR'}, f"Color schema file not found: {colorfile}")
Expand All @@ -690,6 +709,8 @@ def import_gdsii(self, context, filepath):
# Import each layer from the stack
imported_count = 0
for layer_name, data in layerstack.items():
if layer_name=="pdk_config":
continue
z = data['z'] * self.z_scale
height = data['height'] * self.z_scale
layer_index = (data['index'], data['type'])
Expand Down Expand Up @@ -743,7 +764,7 @@ def menu_func_import(self, context):

# Properties to store PDK settings in scene
def register_properties():
bpy.types.Scene.gdsii_pdk_selection = StringProperty(default='IHP_SG13G2')
bpy.types.Scene.gdsii_pdk_selection = StringProperty(default=next(iter(PDK_CONFIGS)))
bpy.types.Scene.gdsii_use_custom_config = BoolProperty(default=False)
bpy.types.Scene.gdsii_custom_config_path = StringProperty(default='')
bpy.types.Scene.gdsii_custom_color_path = StringProperty(default='')
Expand Down
6 changes: 6 additions & 0 deletions import_gdsii/configs/gf180mcu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

pdk_config:
name: GlobalFoundries GF180MCU PDK
description: GlobalFoundries GF180MCU 180nm process
order: 4
Comment thread
Gerard-Taulats marked this conversation as resolved.
Resolved
def_color: realistic

Comp:
index: 22
type: 0
Expand Down
6 changes: 6 additions & 0 deletions import_gdsii/configs/ihp-sg13cmos5l.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

pdk_config:
name: IHP Open PDK SG13CMOS5L
description: IHP SG13CMOS5L 130nm CMOS5L process
order: 2
def_color: realistic

Activ:
index: 1
type: 0
Expand Down
6 changes: 6 additions & 0 deletions import_gdsii/configs/ihp-sg13g2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

pdk_config:
name: IHP Open PDK SG13G2
description: IHP SG13G2 130nm BiCMOS process
order: 1
def_color: realistic

Activ:
index: 1
type: 0
Expand Down
8 changes: 8 additions & 0 deletions import_gdsii/configs/lnoi400.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
# - 2 µm SiO2 upper cladding above the LN device layer
# Only patterned layers have GDS index/type pairs and are extruded below.

pdk_config:
name: Luxtelligence LNOI400 PDK
description: >
Luxtelligence LNOI400 thin-film lithium niobate (X-cut, 400 nm LN, 200 nm etch)
photonics PDK
order: 6
def_color: realistic

# Patterned LN ridge waveguide (200 nm ridge on top of slab)
LN_RIDGE:
index: 2
Expand Down
6 changes: 6 additions & 0 deletions import_gdsii/configs/siepic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
# Stack assumes a 220 nm Si device layer on buried oxide, with SiN above the Si
# in the upper cladding, then TiW heater (M1), tungsten via (VC), and Al routing (M2).

pdk_config:
name: SiEPIC EBeam PDK
description: SiEPIC EBeam silicon photonics PDK (220 nm SOI)
order: 5
def_color: realistic

# 220 nm patterned silicon waveguide layer
Si:
index: 1
Expand Down
6 changes: 6 additions & 0 deletions import_gdsii/configs/sky130.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

pdk_config:
name: SkyWater SKY130 PDK
description: SkyWater SKY130 130nm process
order: 3
def_color: realistic

nwell:
index: 64
type: 20
Expand Down
2 changes: 1 addition & 1 deletion scripts/internal/check_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

pdk_file = yaml.safe_load(process.read_text(encoding='utf-8'))
validate(pdk_file, pdk_schema)
pdk_layers = set(pdk_file.keys())
pdk_layers = set(key for key in pdk_file.keys() if not key=="pdk_config")

for color in (pdks / f"colors/{process.stem}").glob('*yaml'):
print(f" Found color schema: {color.stem}")
Expand Down
23 changes: 22 additions & 1 deletion scripts/internal/layer-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,29 @@
"title": "Layer Configuration",
"description": "Schema for validating layer configuration YAML files with index, type, z-position, and height",
"type": "object",
"properties": {
"pdk_config": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"order": {
"type": "integer"
},
"def_color": {
"type": "string"
}
},
"additionalProperties": false
}
},

"patternProperties": {
"^[A-Za-z0-9_]+$": {
"^(?!pdk_config$)[A-Za-z0-9_]+$": {
"type": "object",
"required": ["index", "type", "z", "height"],
"properties": {
Expand Down
Loading