Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ 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
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
147 changes: 89 additions & 58 deletions import_gdsii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,38 +30,58 @@
# ============================================================================

# 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 = {}
pdk_order = None

for config_path in configs_dir.iterdir():
if not(config_path.is_file() and
config_path.suffix.lower() in [".yaml", ".yml"]):
continue

file_name = config_path.stem

# Get pdk_order
if file_name == "_config_order":
with open(config_path) as f:
pdk_order = yaml.safe_load(f)
continue

key = file_name.upper().replace('-', '_') # Expected key format

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, def_color
pdks[key].update(**config["pdk_config"])

pdks[key].setdefault("name", key)
pdks[key].setdefault("description", key)
pdks[key].setdefault("def_color", "")
# Sort "pdks" dictionary
if pdk_order is None:
return dict(sorted(pdks.items()))

sorted_pdks = {}
for item in pdk_order:
if item=="_": # Get all pdks not listed in pdk_order
remaining = {
k: v
for k, v in pdks.items()
if k not in pdk_order
}
sorted_pdks.update(dict(sorted(remaining.items())))
elif item in pdks.keys():

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: _config_order.yaml lists raw file stems (ihp-sg13g2), but pdks is keyed by the uppercased form (IHP_SG13G2) from line 50 — so item in pdks.keys() and k not in pdk_order (line 77) never match, and custom ordering silently falls back to alphabetical every time. Normalize item/pdk_order entries with the same .upper().replace('-', '_') transform used on line 50 before comparing.

sorted_pdks[item] = pdks[item]
return sorted_pdks

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 +252,9 @@ 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():
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 +357,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 +397,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 +434,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 +528,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 +622,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 +635,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 +647,7 @@ 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', f"configs/{pdk_info["file_name"]}.yaml")

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: nested " inside an f-string delimited by " is a SyntaxError before Python 3.12 — this addon targets Blender 3.4+ (older Python), so the module won't even import.

Suggested change
yamlfile = addon_dir / pdk_info.get('config_path', f"configs/{pdk_info["file_name"]}.yaml")
yamlfile = addon_dir / pdk_info.get('config_path', f'configs/{pdk_info["file_name"]}.yaml')


# Load layer stack configuration
if not yamlfile.is_file():
Expand Down Expand Up @@ -668,7 +697,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', f"configs/colors/{pdk_info["file_name"]}")

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: same nested-quote SyntaxError as the yamlfile line above.

Suggested change
color_dir = addon_dir / pdk_info.get('color_path', f"configs/colors/{pdk_info["file_name"]}")
color_dir = addon_dir / pdk_info.get('color_path', f'configs/colors/{pdk_info["file_name"]}')

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 +719,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 +774,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
11 changes: 11 additions & 0 deletions import_gdsii/configs/_config_order.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 aesc silicon
#
# SPDX-License-Identifier: GPL-3.0-or-later

- _
- ihp-sg13g2
- ihp-sg13cmos5l
- sky130
- gf180mcu
- siepic
- lnoi400
5 changes: 5 additions & 0 deletions import_gdsii/configs/gf180mcu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

pdk_config:
name: GlobalFoundries GF180MCU PDK
description: GlobalFoundries GF180MCU 180nm process
def_color: realistic

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

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

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

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

Activ:
index: 1
type: 0
Expand Down
7 changes: 7 additions & 0 deletions import_gdsii/configs/lnoi400.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
# - 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
def_color: realistic

# Patterned LN ridge waveguide (200 nm ridge on top of slab)
LN_RIDGE:
index: 2
Expand Down
5 changes: 5 additions & 0 deletions import_gdsii/configs/siepic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
# 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)
def_color: realistic

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

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

nwell:
index: 64
type: 20
Expand Down
9 changes: 8 additions & 1 deletion scripts/internal/check_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@
with open(script_dir / 'color-schema.schema.json', encoding='utf-8') as f:
color_schema = json.load(f)

with open(script_dir / 'pdk-order.schema.json', encoding='utf-8') as f:
order_schema = json.load(f)


project_dir = script_dir.parent.parent
pdks = project_dir / "import_gdsii/configs"
for process in pdks.glob('*yaml'):
print(f"Found process: {process.stem}")

pdk_file = yaml.safe_load(process.read_text(encoding='utf-8'))
if process.name == "_config_order.yaml":
validate(pdk_file, order_schema)
continue

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
Loading
Loading