Skip to content

Add layerwise and lazy tiled clipping#147

Open
gpeairs wants to merge 12 commits intomainfrom
gp/clipping
Open

Add layerwise and lazy tiled clipping#147
gpeairs wants to merge 12 commits intomainfrom
gp/clipping

Conversation

@gpeairs
Copy link
Member

@gpeairs gpeairs commented Feb 6, 2026

Update of #41 using a lazy iterator over tiles (giving up on healing). Adds layerwise booleans xor2d_layerwise etc applied across pairs of GeometryStructures.

Closes #100 and #30.

@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 93.36384% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/clipping.jl 92.99% 29 Missing ⚠️

📢 Thoughts on this report? Let us know!

@gpeairs
Copy link
Member Author

gpeairs commented Feb 12, 2026

Example with DemoQPU17, taking the xor2d_layerwise of the artwork before saving vs after a save/load roundtrip that snaps cell contents to 1nm grids:

using FileIO
include("DemoQPU17.jl")
schematic, artwork = DemoQPU17.qpu17_demo(savegds=true)
roundtrip_artwork = load("qpu17_demo.gds")["qpu17_demo"] # Everything rounded to 1nm grid
@time result = xor2d_layerwise(artwork, roundtrip_artwork)
123.436378 seconds (17.01 M allocations: 1.358 GiB, 0.61% gc time)
Dict{GDSMeta, ClippedPolygon{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}} with 4 entries:
  GDSMeta(10, 0) => ClippedPolygon{Quantity{Float64, 𝐋, ContextUnits{(nm,), 𝐋, FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Top-level PolyNode with 0 immediate children.)
  GDSMeta(21, 0) => ClippedPolygon{Quantity{Float64, 𝐋, ContextUnits{(nm,), 𝐋, FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Top-level PolyNode with 1449 immediate children.)
  GDSMeta(20, 0) => ClippedPolygon{Quantity{Float64, 𝐋, ContextUnits{(nm,), 𝐋, FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Top-level PolyNode with 1449 immediate children.)
  GDSMeta(1, 2)  => ClippedPolygon{Quantity{Float64, 𝐋, ContextUnits{(nm,), 𝐋, FreeUnits{(nm,), 𝐋, nothing}, nothing}}}(Top-level PolyNode with 15401 immediate children.)

It's actually a pretty cool effect, effectively leaving sliver polygons only on the boundaries between off-grid points:

image

The junction pattern (layer 10) has a clean XOR because those rectangles are already on an integer grid in artwork. Everything else is affected by rounding, but we can see that it's all within 1nm tolerance:

to_polygons(result[GDSMeta(1,2)]) |> filter(p -> Polygons.area(p) / perimeter(p) > 1nm) # !is_sliver(p)
Polygon{Unitful.Quantity{Float64, 𝐋, Unitful.ContextUnits{(nm,), 𝐋, Unitful.FreeUnits{(nm,), 𝐋, nothing}, nothing}}}[]

We can also do this with tiling:

@time result = xor2d_layerwise(artwork, roundtrip_artwork, max_tile_size=1mm)
@time collected_result = collect(result[GDSMeta(1, 2)])
 0.130062 seconds (604.86 k allocations: 113.544 MiB)
 7.059415 seconds (18.67 M allocations: 1.411 GiB, 6.21% gc time)

The first step is fast because it hasn't done any clipping yet, but the second step (which does the clipping) is also much faster than the non-tiled version. (This is only the metal negative layer, but the results from the other layers are very fast to collect in comparison.)

Let's count up total polygons and non-sliver polygons to see if it's consistent:

total = 0
non_sliver = 0
for tile_clip in collected_result
    polys = to_polygons(tile_clip)
    total += length(polys)
    non_sliver += count(.!(Polygons.is_sliver.(polys)))
end
@show total
@show non_sliver
total = 17779
non_sliver = 0

We get more total polygons because polygons that touch a tile edge are double-counted, but the result is basically equivalent.

@gpeairs gpeairs marked this pull request as ready for review February 12, 2026 14:33
@gpeairs
Copy link
Member Author

gpeairs commented Feb 12, 2026

I don't like how the return type is Dict{Meta, ClippedPolygon} if not tiled and Dict{Meta, Iterator{ClippedPolygon}} if tiled. Should these just be separate functions? Should the non-tiled one be wrapped as a single-element iterable? Should the iterator be collected and combined into one ClippedPolygon (but then you lose some of the advantage of working lazily tile-by-tile)?

Switched it to wrapping the non-tiled result as a single-element vector. The main use case should be comparing full layouts, and that should really be done tiled anyway for performance reasons, but at least this way you can directly swap to the non-tiled version for testing if you're worried about edge artifacts.

Also just use the exact tile size in the keyword argument, don't try to be clever and fit it to the layout.

@gpeairs gpeairs mentioned this pull request Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2D Boolean interface issues

1 participant