Skip to content

Commit b068186

Browse files
mahlau-flexyaugenst-flex
authored andcommitted
feat(io): added pixel_exact option to gds export
1 parent 71ff71e commit b068186

File tree

4 files changed

+113
-21
lines changed

4 files changed

+113
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Added autograd support for `TriangleMesh`, allowing gradient computation with respect to mesh vertices for inverse design.
1414
- Added `EMESimulation.store_coeffs` to store coefficients from the EME solver, including mode overlaps, interface S matrices, and effective propagation indices.
1515
- Get cell-related information from violation markers in `DRCResults` and `DRCViolation` to the klayout plugin: Use for example `DRCResults.violations_by_cell` to group them.
16+
- `pixel_exact` option for `to_gds` export of simulations and structures. If this option is set, any custom medium will be exported as rectangular pixels according to the specification of the medium coordinates.
1617

1718
### Changed
1819
- Removed validator that would warn if `PerturbationMedium` values could become numerically unstable, since an error will anyway be raised if this actually happens when the medium is converted using actual perturbation data.

tests/test_components/test_structure.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,27 @@ def f(x):
264264

265265
grad = ag.grad(f)(1.0)
266266
assert not np.isclose(grad, 0.0)
267+
268+
269+
def test_to_gdstk_pixel_exact(tmp_path):
270+
box = td.Box(center=(0, 0, 0), size=(2, 2, 2))
271+
nx, ny = 50, 50
272+
arr = np.ones((nx, ny, 1))
273+
arr[20:30, 20:30] = 2
274+
x = np.linspace(-1, 1, nx)
275+
y = np.linspace(-1, 1, ny)
276+
z = np.asarray([0])
277+
coords = {"x": x, "y": y, "z": z}
278+
permittivity = td.SpatialDataArray(arr, coords=coords)
279+
280+
medium = td.CustomMedium(permittivity=permittivity)
281+
structure = td.Structure(geometry=box, medium=medium)
282+
polygons = structure.to_gdstk(z=0, frequency=3e14, permittivity_threshold=1.5, pixel_exact=True)
283+
assert polygons
284+
285+
fname = str(tmp_path / "structure-exact.gds")
286+
structure.to_gds_file(fname, z=0, permittivity_threshold=1.5, frequency=3e14, pixel_exact=True)
287+
cells = gdstk.read_gds(fname).cells
288+
cell = cells[0]
289+
assert np.allclose(cell.bounding_box(), ((-0.2, -0.2), (0.2, 0.2)), atol=0.01)
290+
assert gdstk.inside([(0.1, 0.9), (0.5, 2.5), (0, 0)], cell.polygons) == (False, False, True)

tidy3d/components/simulation.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5145,6 +5145,7 @@ def to_gdstk(
51455145
gds_layer_dtype_map: Optional[
51465146
dict[AbstractMedium, tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt]]
51475147
] = None,
5148+
pixel_exact: bool = False,
51485149
) -> list:
51495150
"""Convert a simulation's planar slice to a .gds type polygon list.
51505151
@@ -5163,6 +5164,8 @@ def to_gdstk(
51635164
Frequency for permittivity evaluation in case of custom medium (Hz).
51645165
gds_layer_dtype_map : Dict
51655166
Dictionary mapping mediums to GDSII layer and data type tuples.
5167+
pixel_exact : bool = False
5168+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
51665169
51675170
Return
51685171
------
@@ -5194,6 +5197,7 @@ def to_gdstk(
51945197
frequency=frequency,
51955198
gds_layer=gds_layer,
51965199
gds_dtype=gds_dtype,
5200+
pixel_exact=pixel_exact,
51975201
):
51985202
pmin, pmax = polygon.bounding_box()
51995203
if pmin[0] < bmin[0] or pmin[1] < bmin[1] or pmax[0] > bmax[0] or pmax[1] > bmax[1]:
@@ -5216,6 +5220,7 @@ def to_gds(
52165220
gds_layer_dtype_map: Optional[
52175221
dict[AbstractMedium, tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt]]
52185222
] = None,
5223+
pixel_exact: bool = False,
52195224
) -> None:
52205225
"""Append the simulation structures to a .gds cell.
52215226
@@ -5236,6 +5241,8 @@ def to_gds(
52365241
Frequency for permittivity evaluation in case of custom medium (Hz).
52375242
gds_layer_dtype_map : Dict
52385243
Dictionary mapping mediums to GDSII layer and data type tuples.
5244+
pixel_exact : bool = False
5245+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
52395246
"""
52405247
if gds_layer_dtype_map is None:
52415248
gds_layer_dtype_map = {}
@@ -5248,6 +5255,7 @@ def to_gds(
52485255
permittivity_threshold=permittivity_threshold,
52495256
frequency=frequency,
52505257
gds_layer_dtype_map=gds_layer_dtype_map,
5258+
pixel_exact=pixel_exact,
52515259
)
52525260
if len(polygons) > 0:
52535261
cell.add(*polygons)
@@ -5271,6 +5279,7 @@ def to_gds_file(
52715279
dict[AbstractMedium, tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt]]
52725280
] = None,
52735281
gds_cell_name: str = "MAIN",
5282+
pixel_exact: bool = False,
52745283
) -> None:
52755284
"""Append the simulation structures to a .gds cell.
52765285
@@ -5293,6 +5302,8 @@ def to_gds_file(
52935302
Dictionary mapping mediums to GDSII layer and data type tuples.
52945303
gds_cell_name : str = 'MAIN'
52955304
Name of the cell created in the .gds file to store the geometry.
5305+
pixel_exact : bool = False
5306+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
52965307
"""
52975308
if gdstk_available:
52985309
library = gdstk.Library()
@@ -5326,6 +5337,7 @@ def to_gds_file(
53265337
permittivity_threshold=permittivity_threshold,
53275338
frequency=frequency,
53285339
gds_layer_dtype_map=gds_layer_dtype_map,
5340+
pixel_exact=pixel_exact,
53295341
)
53305342
fname = pathlib.Path(fname)
53315343
fname.parent.mkdir(parents=True, exist_ok=True)

tidy3d/components/structure.py

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ def to_gdstk(
424424
frequency: pydantic.PositiveFloat = 0,
425425
gds_layer: pydantic.NonNegativeInt = 0,
426426
gds_dtype: pydantic.NonNegativeInt = 0,
427+
pixel_exact: bool = False,
427428
) -> None:
428429
"""Convert a structure's planar slice to a .gds type polygon.
429430
@@ -435,15 +436,16 @@ def to_gdstk(
435436
Position of plane in y direction, only one of x,y,z can be specified to define plane.
436437
z : float = None
437438
Position of plane in z direction, only one of x,y,z can be specified to define plane.
438-
permittivity_threshold : float = 1.1
439-
Permitivitty value used to define the shape boundaries for structures with custom
440-
medim
439+
permittivity_threshold : float = 1
440+
Permitivitty value used to define the shape boundaries for structures with custom medium
441441
frequency : float = 0
442442
Frequency for permittivity evaluaiton in case of custom medium (Hz).
443443
gds_layer : int = 0
444444
Layer index to use for the shapes stored in the .gds file.
445445
gds_dtype : int = 0
446446
Data-type index to use for the shapes stored in the .gds file.
447+
pixel_exact : bool = False
448+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
447449
448450
Return
449451
------
@@ -457,27 +459,72 @@ def to_gdstk(
457459
axis, _ = self.geometry.parse_xyz_kwargs(x=x, y=y, z=z)
458460
bb_min, bb_max = self.geometry.bounds
459461

460-
# Set the contour scale to be the minimal cooridante step size w.r.t. the 3 main axes,
461-
# skipping those with a single coordniate. In case all axes have only a single coordinate,
462-
# use the largest bounding box dimension.
463462
eps, _, _ = self.medium.eps_dataarray_freq(frequency=frequency)
464-
scale = max(b - a for a, b in zip(bb_min, bb_max))
465-
for coord in (eps.x, eps.y, eps.z):
466-
if len(coord) > 1:
467-
scale = min(scale, np.diff(coord).min())
468-
469-
coords = Coords(
470-
x=np.arange(bb_min[0], bb_max[0] + scale * 0.9, scale) if x is None else x,
471-
y=np.arange(bb_min[1], bb_max[1] + scale * 0.9, scale) if y is None else y,
472-
z=np.arange(bb_min[2], bb_max[2] + scale * 0.9, scale) if z is None else z,
473-
)
463+
if pixel_exact:
464+
coords = Coords(
465+
x=eps.x if x is None else x,
466+
y=eps.y if y is None else y,
467+
z=eps.z if z is None else z,
468+
)
469+
else:
470+
# Set the contour scale to be the minimal cooridante step size w.r.t. the 3 main axes,
471+
# skipping those with a single coordniate. In case all axes have only a single coordinate,
472+
# use the largest bounding box dimension.
473+
scale = max(b - a for a, b in zip(bb_min, bb_max))
474+
for coord in (eps.x, eps.y, eps.z):
475+
if len(coord) > 1:
476+
scale = min(scale, np.diff(coord).min())
477+
coords = Coords(
478+
x=np.arange(bb_min[0], bb_max[0] + scale * 0.9, scale) if x is None else x,
479+
y=np.arange(bb_min[1], bb_max[1] + scale * 0.9, scale) if y is None else y,
480+
z=np.arange(bb_min[2], bb_max[2] + scale * 0.9, scale) if z is None else z,
481+
)
482+
474483
eps = self.medium.eps_diagonal_on_grid(frequency=frequency, coords=coords)
475-
eps = np.stack((eps[0].real, eps[1].real, eps[2].real), axis=3).max(axis=3).squeeze()
476-
contours = gdstk.contour(eps.T, permittivity_threshold, scale, precision=scale * 1e-3)
484+
eps = (
485+
np.stack((eps[0].real, eps[1].real, eps[2].real), axis=3)
486+
.max(axis=3)
487+
.squeeze(axis=axis)
488+
)
489+
490+
if pixel_exact:
491+
# Convert coordinates to numpy arrays for efficient processing
492+
_, (w, h) = self.geometry.pop_axis((coords.x, coords.y, coords.z), axis)
493+
w, h = np.asarray(w), np.asarray(h)
494+
_, (wmin, hmin) = self.geometry.pop_axis(bb_min, axis)
495+
_, (wmax, hmax) = self.geometry.pop_axis(bb_max, axis)
496+
497+
# Determine boundaries by taking the midpoint between adjacent coordinates
498+
if w.size > 1:
499+
dw = np.diff(w) * 0.5
500+
wb = np.concatenate(([wmin], w[:-1] + dw, [wmax]))
501+
else:
502+
wb = np.array([wmin, wmax])
503+
504+
if h.size > 1:
505+
dh = np.diff(h) * 0.5
506+
hb = np.concatenate(([hmin], h[:-1] + dh, [hmax]))
507+
else:
508+
hb = np.array([hmin, hmax])
509+
510+
# Create boolean mask where permittivity exceeds threshold
511+
mask = eps > permittivity_threshold
512+
w_idxs, h_idxs = np.where(mask)
513+
514+
# Generate list of gdstk.Polygon (rectangles)
515+
contours = [
516+
gdstk.rectangle((wb[wi], hb[hi]), (wb[wi + 1], hb[hi + 1]))
517+
for wi, hi in zip(w_idxs, h_idxs)
518+
]
519+
520+
else:
521+
contours = gdstk.contour(
522+
eps.T, permittivity_threshold, scale, precision=scale * 1e-3
523+
)
477524

478-
_, (dx, dy) = self.geometry.pop_axis(bb_min, axis)
479-
for polygon in contours:
480-
polygon.translate(dx, dy)
525+
_, (dx, dy) = self.geometry.pop_axis(bb_min, axis)
526+
for polygon in contours:
527+
polygon.translate(dx, dy)
481528

482529
polygons = gdstk.boolean(polygons, contours, "and", layer=gds_layer, datatype=gds_dtype)
483530

@@ -493,6 +540,7 @@ def to_gds(
493540
frequency: pydantic.PositiveFloat = 0,
494541
gds_layer: pydantic.NonNegativeInt = 0,
495542
gds_dtype: pydantic.NonNegativeInt = 0,
543+
pixel_exact: bool = False,
496544
) -> None:
497545
"""Append a structure's planar slice to a .gds cell.
498546
@@ -515,6 +563,8 @@ def to_gds(
515563
Layer index to use for the shapes stored in the .gds file.
516564
gds_dtype : int = 0
517565
Data-type index to use for the shapes stored in the .gds file.
566+
pixel_exact : bool = False
567+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
518568
"""
519569
if not isinstance(cell, gdstk.Cell):
520570
if "gdstk" in cell.__class__.__name__.lower() and not gdstk_available:
@@ -531,6 +581,7 @@ def to_gds(
531581
frequency=frequency,
532582
gds_layer=gds_layer,
533583
gds_dtype=gds_dtype,
584+
pixel_exact=pixel_exact,
534585
)
535586
if polygons:
536587
cell.add(*polygons)
@@ -546,6 +597,7 @@ def to_gds_file(
546597
gds_layer: pydantic.NonNegativeInt = 0,
547598
gds_dtype: pydantic.NonNegativeInt = 0,
548599
gds_cell_name: str = "MAIN",
600+
pixel_exact: bool = False,
549601
) -> None:
550602
"""Export a structure's planar slice to a .gds file.
551603
@@ -570,6 +622,8 @@ def to_gds_file(
570622
Data-type index to use for the shapes stored in the .gds file.
571623
gds_cell_name : str = 'MAIN'
572624
Name of the cell created in the .gds file to store the geometry.
625+
pixel_exact : bool = False
626+
If true export gds as pixel exact rectangles instead of gdstk contour if a custom medium is provided.
573627
"""
574628
try:
575629
import gdstk
@@ -590,6 +644,7 @@ def to_gds_file(
590644
frequency=frequency,
591645
gds_layer=gds_layer,
592646
gds_dtype=gds_dtype,
647+
pixel_exact=pixel_exact,
593648
)
594649
fname = pathlib.Path(fname)
595650
fname.parent.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)