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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
ThreadSafeDicts = "4239201d-c60e-5e0a-9702-85d713665ba7"
Expand Down Expand Up @@ -71,6 +72,7 @@ PkgTemplates = "0.7"
PrecompileTools = "1"
Preferences = "1"
QuadGK = "2"
SHA = "0.7.0"
SpatialIndexing = "0.1"
StaticArrays = "1"
TestItemRunner = "1.1.0"
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ See [Shapes](./shapes.md).
GDSMeta
GDSWriterOptions
gdslayers(::Cell)
Cells.geometry_fingerprint
render!(::Cell, ::Polygon, ::GDSMeta)
render!(::Cell, ::DeviceLayout.GeometryStructure)
DeviceLayout.save(::File{format"GDS"}, ::Cell, ::Cell...)
Expand Down
74 changes: 55 additions & 19 deletions src/backends/gds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ using ..Points
import ..Rectangles: Rectangle
import ..Polygons: Polygon
using ..Cells
import ..Cells: p2p
using ..Texts

import FileIO: File, @format_str, stream, magic, skipmagic
Expand Down Expand Up @@ -78,6 +79,8 @@ Options controlling warnings and validation during GDS file writing.
case-insensitive (GDS readers like KLayout treat cell names case-insensitively).
The original `Cell` objects are not mutated; renamed names are only written to the file.
New names may exceed the GDSII spec's 32 character limit.
- `normalize::Bool = false`: If `true`, save with a fixed timestamp and a canonical sorting
of elements to allow geometry validation by bitwise file comparison.

Warnings for layer number and datatype are configurable because different tools may have
different limits. In the GDSII specification, layer and datatype must be in the range 0 to 63,
Expand Down Expand Up @@ -112,6 +115,7 @@ opts = GDSWriterOptions(rename_duplicates=true)
max_datatype::Int = 32767
warn_invalid_names::Bool = true
rename_duplicates::Bool = false
normalize::Bool = false
end

const GDSTokens = Dict{UInt16, String}(
Expand Down Expand Up @@ -367,15 +371,16 @@ function gdswrite(
)
name = even(get(name_map, cell, cell.name))
options.warn_invalid_names && namecheck(name)
create = options.normalize ? unix2datetime(0) : cell.create

y = UInt16(Dates.value(Dates.Year(cell.create)))
mo = UInt16(Dates.value(Dates.Month(cell.create)))
d = UInt16(Dates.value(Dates.Day(cell.create)))
h = UInt16(Dates.value(Dates.Hour(cell.create)))
min = UInt16(Dates.value(Dates.Minute(cell.create)))
s = UInt16(Dates.value(Dates.Second(cell.create)))
y = UInt16(Dates.value(Dates.Year(create)))
mo = UInt16(Dates.value(Dates.Month(create)))
d = UInt16(Dates.value(Dates.Day(create)))
h = UInt16(Dates.value(Dates.Hour(create)))
min = UInt16(Dates.value(Dates.Minute(create)))
s = UInt16(Dates.value(Dates.Second(create)))

modify = now()
modify = options.normalize ? unix2datetime(0) : now()
y1 = UInt16(Dates.value(Dates.Year(modify)))
mo1 = UInt16(Dates.value(Dates.Month(modify)))
d1 = UInt16(Dates.value(Dates.Day(modify)))
Expand All @@ -385,21 +390,44 @@ function gdswrite(

bytes = gdswrite(io, BGNSTR, y, mo, d, h, min, s, y1, mo1, d1, h1, min1, s1)
bytes += gdswrite(io, STRNAME, name)
for (x, m) in zip(cell.elements, cell.element_metadata)
bytes += gdswrite(io, x, m, dbs, options)
end
for x in cell.refs
bytes += gdswrite(io, x, dbs; name_map)
end
for (x, m) in zip(cell.texts, cell.text_metadata)
bytes += gdswrite(io, x, m, dbs, options)
if !options.normalize
for (x, m) in zip(cell.elements, cell.element_metadata)
bytes += gdswrite(io, x, m, dbs, options)
end
for x in cell.refs
bytes += gdswrite(io, x, dbs; name_map)
end
for (x, m) in zip(cell.texts, cell.text_metadata)
bytes += gdswrite(io, x, m, dbs, options)
end
else
# Normalize (order same as Cells.geometry_fingerprint except refs not flattened)
els_metas = [
(Polygon(circshift(poly.p, 1 - argmin(poly.p))), meta) for
(poly, meta) in zip(cell.elements, cell.element_metadata)
]
for (x, m) in sort(
els_metas;
by=xm -> (gdslayer(last(xm)), datatype(last(xm)), points(first(xm)))
)
bytes += gdswrite(io, x, m, dbs, options)
end
for x in
sort(cell.refs; by=r -> (r.structure.name, r.origin, r.xrefl, r.rot, r.mag))
bytes += gdswrite(io, x, dbs; name_map)
end
texts_metas = collect(zip(cell.texts, cell.text_metadata))
for (x, m) in sort(
texts_metas;
by=xm ->
(gdslayer(last(xm)), datatype(last(xm)), first(xm).text, first(xm).origin)
)
bytes += gdswrite(io, x, m, dbs, options)
end
end
return bytes += gdswrite(io, ENDSTR)
end

p2p(x::Length, dbs) = convert(Int, round(convert(Float64, x / dbs)))
p2p(x::Real, dbs) = p2p(x * 1μm, dbs)

"""
gdswrite(io::IO, poly::Polygon{T}, meta, dbs, options::GDSWriterOptions=GDSWriterOptions()) where {T}

Expand Down Expand Up @@ -647,9 +675,14 @@ function save(
max_layer=typemax(UInt16),
max_datatype=typemax(UInt16),
warn_invalid_names=false,
rename_duplicates=options.rename_duplicates
rename_duplicates=options.rename_duplicates,
normalize=options.normalize
)
end
if options.normalize
modify = unix2datetime(0)
acc = unix2datetime(0)
end
dbs = dbscale(cell0, cell...)
pad = mod(length(name), 2) == 1 ? "\0" : ""
open(f, "w") do s
Expand All @@ -669,6 +702,9 @@ function save(
print("\n")
end
ordered = order!(a)
if options.normalize
sort!(ordered, by=x -> x.name)
end
if verbose
@info("Cells written in order:")
display(ordered)
Expand Down
72 changes: 70 additions & 2 deletions src/cells.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ using Dates
using Unitful
import Unitful: Length

using SHA

import ..Texts

import DeviceLayout
Expand All @@ -20,11 +22,20 @@ import DeviceLayout:
Transformations,
UPREFERRED
import DeviceLayout:
aref, sref, gdslayer, layer, nm, elements, element_metadata, refs, render!
aref, sref, gdslayer, layer, μm, nm, elements, element_metadata, refs, render!
import DeviceLayout: flatten, flatten!, order!, traverse!, uniquename # to re-export

export Cell, CellArray, CellReference
export cell, dbscale, layers, gdslayers, flatten, flatten!, order!, traverse!, uniquename
export cell,
dbscale,
layers,
gdslayers,
geometry_fingerprint,
flatten,
flatten!,
order!,
traverse!,
uniquename

# Avoid circular definitions
abstract type AbstractCell{S} <: AbstractCoordinateSystem{S} end
Expand Down Expand Up @@ -311,4 +322,61 @@ end

Base.isempty(c::Cell) = isempty(elements(c)) && isempty(refs(c)) && isempty(c.texts)

p2p(x::Length, dbs) = convert(Int, round(convert(Float64, x / dbs)))
p2p(x::Real, dbs) = p2p(x * 1μm, dbs)

"""
geometry_fingerprint(cell::Cell) -> String

Deterministic SHA-256 of a Cell's geometry. Normalizes by flattening all
references, sorting elements by (layer, datatype, vertices), and hashing
the canonical byte representation of polygons and their metadata.

Coordinates are converted to Int32 in the cell's database unit. Polygons
vertices are `circshift`ed to start with the lowest of leftmost points.
Texts are included in the hash, but only their string, origin, and metadata
are hashed, not their other attributes.
"""
function geometry_fingerprint(c::Cell)
flat = flatten(c)
dbs = dbscale(c)
# Polygons: (layer, datatype, [(x,y)...])
polys = map(zip(element_metadata(flat), elements(flat))) do (meta, poly)
coords = [Point(Int32(p2p(p.x, dbs)), Int32(p2p(p.y, dbs))) for p in poly.p]
return (
Int32(meta.layer),
Int32(meta.datatype),
circshift(coords, 1 - argmin(coords))
)
end
sort!(polys)

ctx = SHA256_CTX()

# Hash polygons
for (layer, dt, coords) in polys
update!(ctx, reinterpret(UInt8, [layer, dt]))
update!(ctx, reinterpret(UInt8, [Int32(length(coords))]))
update!(ctx, reinterpret(UInt8, coords))
end

# Hash texts (labels/ports can change too), only worry about origin and string
texts_data = map(zip(flat.text_metadata, flat.texts)) do (m, t)
return (
Int32(m.layer),
Int32(m.datatype),
[Int32(p2p(t.origin.x, dbs)), Int32(p2p(t.origin.y, dbs))],
t.text
)
end
sort!(texts_data)
for (layer, dt, og, s) in texts_data
update!(ctx, reinterpret(UInt8, [layer, dt]))
update!(ctx, reinterpret(UInt8, og))
update!(ctx, Vector{UInt8}(s))
end

return bytes2hex(digest!(ctx))
end

end # module
28 changes: 28 additions & 0 deletions test/tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,34 @@ end
joinpath(@__DIR__, "propattr.gds");
verbose=true
)

# Normalized GDS tests
cf = Cell{Float64}("main")
render!(cf, Rectangle(10, 10), GDSMeta())
render!(cf, Rectangle(20, 20), GDSMeta(1))
addref!(cf, Cell{Float64}("sub1"), Point(10, 10))
addref!(cf, Cell{Float64}("sub2"), Point(20, 20))
text!(cf, "test2", Point(20, 20), GDSMeta(1))
text!(cf, "test", Point(30, 30), GDSMeta())
c = Cell("main", nm)
render!(c, Rectangle(10μm, 10μm), GDSMeta())
render!(c, Rectangle(20μm, 20μm), GDSMeta(1))
addref!(c, Cell("sub2", nm), Point(20, 20)μm)
addref!(c, Cell("sub1", nm), Point(10, 10)μm)
text!(c, "test", Point(30, 30)μm, GDSMeta())
text!(c, "test2", Point(20, 20)μm, GDSMeta(1))
path1 = joinpath(tdir, "test1.gds")
path2 = joinpath(tdir, "test2.gds")
save(path1, cf, options=GDSWriterOptions(normalize=true))
save(path2, c, options=GDSWriterOptions(normalize=true))
@test success(`cmp --quiet $path1 $path2`)
@test Cells.geometry_fingerprint(cf) == Cells.geometry_fingerprint(c)
# regression test -- if this changes, we have to decide whether it's breaking
@test Cells.geometry_fingerprint(c) ==
"5d9a98b1fa77f31f1e658e4be0dd09f90be2d734c32224b82884b857cf08a682"
# negative test
render!(cf, Rectangle(10, 10), GDSMeta())
@test Cells.geometry_fingerprint(cf) != Cells.geometry_fingerprint(c)
end

@testset "DXF format" begin
Expand Down
Loading