Skip to content
Open
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
13 changes: 6 additions & 7 deletions src/kfactory/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,14 @@ def get_affinity() -> int:
On (most) linux we can get it through the scheduling affinity. Otherwise,
fall back to the multiprocessing cpu count.
"""
try:
if hasattr(os, "sched_getaffinity"):
return len(os.sched_getaffinity(0))
except AttributeError:
try:
import multiprocessing
try:
import multiprocessing

return multiprocessing.cpu_count()
except ModuleNotFoundError:
return 1
return multiprocessing.cpu_count()
except ModuleNotFoundError:
return 1


dotenv_path = find_dotenv(usecwd=True)
Expand Down
9 changes: 9 additions & 0 deletions src/kfactory/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"CellNameError",
"CrossSectionNamingConflictError",
"CrossSectionSymmetryMismatchError",
"DuplicateCellNameError",
"FactoriesLockedError",
"InvalidLayerError",
"LockedError",
Expand Down Expand Up @@ -203,5 +204,13 @@ class CellNameError(ValueError):
"""Raised if a KCell is created and the automatic assigned name is taken."""


class DuplicateCellNameError(ValueError):
"""Raised when writing a layout with multiple cells sharing the same name.

GDS/OASIS formats require unique cell names. This error provides details
about which names are duplicated and which cells are involved.
"""


class InvalidLayerError(ValueError):
"""Raised when a layer is not valid."""
203 changes: 94 additions & 109 deletions src/kfactory/kcell.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
TAsymmetricCrossSection,
TCrossSection,
)
from .exceptions import LockedError, MergeError
from .exceptions import DuplicateCellNameError, LockedError, MergeError
from .geometry import DBUGeometricObject, GeometricObject, UMGeometricObject
from .instance import DInstance, Instance, ProtoInstance, ProtoTInstance, VInstance
from .instances import (
Expand Down Expand Up @@ -145,6 +145,51 @@
]


def _deduplicate_cell_names(layout: kdb.Layout, cell_indices: set[int]) -> None:
"""Auto-rename cells with duplicate names so the layout can be written.

GDS/OASIS require unique cell names. When duplicates are found among
`cell_indices`, the first cell keeps its name and subsequent ones get
a ``$1``, ``$2``, … suffix (matching KLayout's own convention).
A warning is logged for each renamed cell.
"""
from collections import defaultdict

name_to_indices: dict[str, list[int]] = defaultdict(list)
for ci in cell_indices:
c = layout.cell(ci)
if c is not None and not c._destroyed():
name_to_indices[c.name].append(ci)

duplicates = {
name: indices for name, indices in name_to_indices.items() if len(indices) > 1
}
if not duplicates:
return

for name, indices in duplicates.items():
# Keep the first cell, rename the rest
for ci in indices[1:]:
c = layout.cell(ci)
if c is None or c._destroyed():
continue
unique = layout.unique_cell_name(name)
was_locked = c.is_locked()
if was_locked:
c.locked = False
c.name = unique
if was_locked:
c.locked = True
logger.warning(
"Renamed duplicate cell {old!r} (cell_index={ci}) to {new!r}"
" before writing. Set `kf.config.debug_names = True` to catch"
" name conflicts earlier.",
old=name,
ci=ci,
new=unique,
)


class BaseKCell(BaseModel, ABC, arbitrary_types_allowed=True):
"""KLayout cell and change its class to KCell.

Expand Down Expand Up @@ -482,125 +527,59 @@ def name(self, value: str) -> None:
and not self.kcl.layout.cell(value).is_library_cell()
and not self.is_library_cell()
):
stack = inspect.stack()
module = inspect.getmodule(stack[3].frame)
tkcells = [
self.kcl.tkcells[cell.cell_index()]
for cell in self.kcl.layout.cells(value)
if not cell.is_library_cell()
]

conflicting = "\n".join(
f" - {tkcell.name!r} (cell_index={tkcell.kdb_cell.cell_index()},"
f" function_name={tkcell.function_name!r},"
f" basename={tkcell.basename!r})"
for tkcell in tkcells
)

stack = inspect.stack()
module = inspect.getmodule(stack[3].frame)

if module is not None and module.__name__ == "kfactory.layout":
frame_info = stack[5]
logger.opt(depth=2).error(
"Name conflict in "
f"{frame_info.frame.f_locals['f'].__code__.co_filename}::"
f"{frame_info.frame.f_locals['f'].__name__} at line "
f"{frame_info.frame.f_locals['f'].__code__.co_firstlineno}\n"
f"Renaming {self.name} (cell_index={self.kdb_cell.cell_index()}) to"
f" {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} (cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
)
if config.debug_names:
raise ValueError(
"Name conflict in "
f"{frame_info.frame.f_locals['f'].__code__.co_filename}::"
f"{frame_info.frame.f_locals['f'].__name__} at line "
f"{frame_info.frame.f_locals['f'].__code__.co_firstlineno}\n"
f"Renaming {self.name} (cell_index={self.kdb_cell.cell_index()}"
f") to {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} "
f"(cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
fi = stack[5]
f_obj = fi.frame.f_locals.get("f")
if f_obj is not None:
location = (
f"{f_obj.__code__.co_filename}::{f_obj.__name__}"
f" at line {f_obj.__code__.co_firstlineno}"
)
else:
location = f"{fi.filename}::{fi.function} at line {fi.lineno}"
log_depth = 2
else:
frame_info = stack[3]
fi = stack[3]
if module is not None:
module_name = module.__name__
if module_name == "__main__":
module_name = frame_info.filename
function_name = (
"::" + frame_info.function
if frame_info.function != "<module>"
else ""
)
logger.opt(depth=3).error(
"Name conflict in "
f"{module_name}{function_name} at line "
f"{frame_info.lineno}\n"
f"Renaming {self.name} (cell_index="
f"{self.kdb_cell.cell_index()}) to"
f" {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} "
f"(cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
)
if config.debug_names:
raise ValueError(
"Name conflict in "
f"{module_name}{function_name} at line "
f"{frame_info.lineno}\n"
f"Renaming {self.name} (cell_index="
f"{self.kdb_cell.cell_index()}) to"
f" {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} "
f"(cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
)
mod_name = module.__name__
if mod_name == "__main__":
mod_name = fi.filename
else:
function_name = (
"::" + frame_info.function
if frame_info.function != "<module>"
else ""
)
logger.opt(depth=3).error(
"Name conflict in "
f"{frame_info.filename}"
f"{function_name} at line {frame_info.lineno}\n"
f"Renaming {self.name} (cell_index="
f"{self.kdb_cell.cell_index()}) to"
f" {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} "
f"(cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
)
if config.debug_names:
raise ValueError(
"Name conflict in "
f"{frame_info.filename}"
f"{function_name} at line {frame_info.lineno}\n"
f"Renaming {self.name} (cell_index="
f"{self.kdb_cell.cell_index()}) to"
f" {value} would cause it to be named the same as:\n"
+ "\n".join(
f" - {tkcell.name} "
f"(cell_index={tkcell.kdb_cell.cell_index()}),"
f" function_name={tkcell.function_name},"
f" basename={tkcell.basename}"
for tkcell in tkcells
)
)
mod_name = fi.filename
func_suffix = f"::{fi.function}" if fi.function != "<module>" else ""
location = f"{mod_name}{func_suffix} at line {fi.lineno}"
log_depth = 3

msg = (
f"Cell name conflict in {location}\n"
f"Renaming {self.name!r}"
f" (cell_index={self.kdb_cell.cell_index()}) to {value!r}"
f" would create a duplicate — the following cell(s) already"
f" have that name:\n{conflicting}\n"
f"This will make the layout unwritable (GDS/OASIS require"
f" unique cell names).\n"
f"Set `kf.config.debug_names = True` to turn this warning"
f" into an error and catch the conflict at its source."
)
logger.opt(depth=log_depth).error(msg)
if config.debug_names:
raise DuplicateCellNameError(msg)

self.kdb_cell.name = value

Expand Down Expand Up @@ -1323,6 +1302,9 @@ def write(
case _:
...

relevant_cells = {self.cell_index(), *self.called_cells()}
_deduplicate_cell_names(self.layout(), relevant_cells)

filename = str(filename)
if autoformat_from_file_extension:
save_options.set_format_from_filename(filename)
Expand Down Expand Up @@ -1367,6 +1349,9 @@ def write_bytes(
case _:
...

relevant_cells = {self.cell_index(), *self.called_cells()}
_deduplicate_cell_names(self.layout(), relevant_cells)

save_options.format = save_options.format or "OASIS"
save_options.clear_cells()
save_options.select_cell(self.cell_index())
Expand Down
46 changes: 46 additions & 0 deletions src/kfactory/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2234,11 +2234,57 @@ def write(
if kcell.is_library_cell() and not kcell.destroyed():
kcell.convert_to_static(recursive=True)

self._deduplicate_cell_names()

if autoformat_from_file_extension:
options.set_format_from_filename(filename)

return self.layout.write(filename, options)

def _deduplicate_cell_names(self) -> None:
"""Auto-rename cells with duplicate names so the layout can be written.

GDS/OASIS require unique cell names. The first cell keeps its name;
subsequent duplicates get ``$1``, ``$2``, … suffixes. A warning is
logged for each rename.
"""
from collections import defaultdict

name_to_cells: dict[str, list[kdb.Cell]] = defaultdict(list)
for c in self.layout.each_cell():
if not c._destroyed():
name_to_cells[c.name].append(c)

duplicates = {
name: cells for name, cells in name_to_cells.items() if len(cells) > 1
}
if not duplicates:
return

for name, cells in duplicates.items():
for c in cells[1:]:
if c._destroyed():
continue
unique = self.layout.unique_cell_name(name)
tkcell = self.tkcells.get(c.cell_index())
fn = tkcell.function_name if tkcell else None
was_locked = c.is_locked()
if was_locked:
c.locked = False
c.name = unique
if was_locked:
c.locked = True
logger.warning(
"Renamed duplicate cell {old!r} (cell_index={ci},"
" function_name={fn!r}) to {new!r} before writing."
" Set `kf.config.debug_names = True` to catch name"
" conflicts earlier.",
old=name,
ci=c.cell_index(),
fn=fn,
new=unique,
)

def top_kcells(self) -> list[KCell]:
"""Return the top KCells."""
return [self[tc.cell_index()] for tc in self.top_cells()]
Expand Down
Loading